Skip to content

Highlight moves when hovering over the copy/delete buttons #17290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f15d3b7
When the user hovers over the `Delete from here` button, highlight th…
johndoknjas Mar 30, 2025
496c005
Merge remote-tracking branch 'upstream/master' into highlight-before-…
johndoknjas Mar 30, 2025
ce725b5
Prevent any possible highlighting when pressing to delete a variation…
johndoknjas Mar 31, 2025
21ba86b
Fix linting error.
johndoknjas Mar 31, 2025
724cfdc
Merge branch 'master' into highlight-before-delete
johndoknjas Mar 31, 2025
a9e565f
Generalize the code for (un)highlighting.
johndoknjas Mar 31, 2025
1b66c4a
More linting stuff.
johndoknjas Mar 31, 2025
a07159b
Merge remote-tracking branch 'origin/copy-variation-pgn' into highlig…
johndoknjas Mar 31, 2025
4fe6a3c
When hovering over the `Copy variation/mainline PGN` button, highligh…
johndoknjas Apr 2, 2025
6fa976a
linting
johndoknjas Apr 2, 2025
9ecf541
Merge branch 'master' into highlight-moves
ornicar Apr 8, 2025
cb41788
Fix bug where path values with a backslash are not handled correctly …
johndoknjas Apr 9, 2025
cb54315
Merge remote-tracking branch 'origin/highlight-moves' into highlight-…
johndoknjas Apr 9, 2025
726c8df
Merge remote-tracking branch 'upstream/master' into highlight-moves
johndoknjas Apr 9, 2025
1fd3b7c
Merge remote-tracking branch 'upstream/master' into highlight-moves
johndoknjas Apr 26, 2025
6e392f3
Make the context menu transparent if the user hovers over the delete …
johndoknjas Apr 26, 2025
f28503b
For highlighting moves in red or green (for pending deletion/copy), u…
johndoknjas Apr 28, 2025
b082d8c
Do not adjust the context menu's classlist directly.
johndoknjas Apr 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ui/analyse/css/_context-menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@
background: $m-accent_bg--mix-10;
}
}

&.transparent {
opacity: 0.7;
transition: opacity 0.2s ease;
}
}
27 changes: 25 additions & 2 deletions ui/analyse/src/ctrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import type { PgnError } from 'chessops/pgn';
import { ChatCtrl } from 'lib/chat/chatCtrl';
import { confirm } from 'lib/view/dialogs';
import api from './api';
import { init } from 'lib/tree/path';

export default class AnalyseCtrl {
data: AnalyseData;
Expand Down Expand Up @@ -116,6 +117,8 @@ export default class AnalyseCtrl {
initialPath: Tree.Path;
contextMenuPath?: Tree.Path;
gamePath?: Tree.Path;
pendingCopyPaths: Set<Tree.Path>;
pendingDeletionPaths: Set<Tree.Path>;

// misc
requestInitialPly?: number; // start ply from the URL location hash
Expand Down Expand Up @@ -159,6 +162,8 @@ export default class AnalyseCtrl {

this.initialPath = this.makeInitialPath();
this.setPath(this.initialPath);
this.pendingCopyPaths = new Set<Tree.Path>();
this.pendingDeletionPaths = new Set<Tree.Path>();

this.showGround();
this.onToggleComputer();
Expand Down Expand Up @@ -618,6 +623,7 @@ export default class AnalyseCtrl {
}

async deleteNode(path: Tree.Path): Promise<void> {
this.deletionHighlightFromHere(path, true);
const node = this.tree.nodeAtPath(path);
if (!node) return;
const count = treeOps.countChildrenAndComments(node);
Expand Down Expand Up @@ -1041,10 +1047,27 @@ export default class AnalyseCtrl {
};

pluginUpdate = (fen: string) => {
// if controller and chessground board state differ, ignore this update. once the chessground
// If controller and chessground board states differ, ignore this update. Once the chessground
// state is updated to match, pluginUpdate will be called again.
if (!fen.startsWith(this.chessground?.getFen())) return;

this.keyboardMove?.update({ fen, canMove: true });
};

deletionHighlightFromHere = (path: Tree.Path, unhighlight: boolean) => {
this.pendingDeletionPaths = new Set<Tree.Path>(
unhighlight ? [] : [path, ...this.tree.getPathsOfDescendants(this.tree.nodeAtPath(path), path)],
);
this.redraw();
};

copyVariationHighlight = (path: Tree.Path, unhighlight: boolean) => {
const paths = [];
if (!unhighlight)
while (path) {
paths.push(path);
path = init(path);
}
this.pendingCopyPaths = new Set<Tree.Path>(paths);
this.redraw();
};
}
6 changes: 1 addition & 5 deletions ui/analyse/src/pgnExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,7 @@ export function renderNodesHtml(nodes: PgnNode[]): MaybeVNodes {
return tags;
}

export function renderVariationPgn(game: Game, nodeList: Tree.Node[], isMainline: boolean): string {
nodeList = [...nodeList];
let newNode;
while ((newNode = nodeList.slice(-1)[0]?.children[0]) && !(isMainline && newNode.forceVariation))
nodeList.push(newNode);
export function renderVariationPgn(game: Game, nodeList: Tree.Node[]): string {
const filteredNodeList = nodeList.filter(node => node.san);
if (filteredNodeList.length === 0) return '';

Expand Down
2 changes: 2 additions & 0 deletions ui/analyse/src/treeView/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export function nodeClasses(ctx: Ctx, node: Tree.Node, path: Tree.Path): NodeCla
good: glyphIds.includes(1),
brilliant: glyphIds.includes(3),
interesting: glyphIds.includes(5),
pendingDeletion: ctx.ctrl.pendingDeletionPaths.has(path),
pendingCopy: ctx.ctrl.pendingCopyPaths.has(path),
};
}

Expand Down
61 changes: 52 additions & 9 deletions ui/analyse/src/treeView/contextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as licon from 'lib/licon';
import { type VNode, bind, onInsert, looseH as h } from 'lib/snabbdom';
import { type VNode, onInsert, looseH as h } from 'lib/snabbdom';
import type AnalyseCtrl from '../ctrl';
import * as studyView from '../study/studyView';
import { patch, nodeFullName } from '../view/util';
import { renderVariationPgn } from '../pgnExport';
import { isTouchDevice } from 'lib/device';

export interface Opts {
path: Tree.Path;
Expand Down Expand Up @@ -51,14 +52,45 @@ function positionMenu(menu: HTMLElement, coords: Coords): void {
: (menu.style.top = coords.y + 'px');
}

function action(icon: string, text: string, handler: () => void): VNode {
return h('a', { attrs: { 'data-icon': icon }, hook: bind('click', handler) }, text);
function action(
icon: string,
text: string,
onClick: () => void,
onHover?: () => void,
onLeave?: () => void,
): VNode {
return h(
'a',
{
attrs: { 'data-icon': icon },
hook: {
insert: vnode => {
const elm = vnode.elm as HTMLElement;
elm.addEventListener('click', onClick);
if (onHover && !isTouchDevice())
elm.addEventListener('mouseover', () => {
onHover();
// If there is a special action for hover, make the menu transparent so that effects
// on the move list can be fully seen:
$('#' + elementId).addClass('transparent');
});
if (onLeave)
elm.addEventListener('mouseout', () => {
onLeave();
$('#' + elementId).removeClass('transparent');
});
},
},
},
text,
);
}

function view(opts: Opts, coords: Coords): VNode {
const ctrl = opts.root,
node = ctrl.tree.nodeAtPath(opts.path),
onMainline = ctrl.tree.pathIsMainline(opts.path) && !ctrl.tree.pathIsForcedVariation(opts.path);
onMainline = ctrl.tree.pathIsMainline(opts.path) && !ctrl.tree.pathIsForcedVariation(opts.path),
extendedPath = opts.root.tree.extendPath(opts.path, onMainline);
return h(
'div#' + elementId + '.visible',
{
Expand All @@ -78,7 +110,13 @@ function view(opts: Opts, coords: Coords): VNode {

!onMainline && action(licon.Checkmark, i18n.site.makeMainLine, () => ctrl.promote(opts.path, true)),

action(licon.Trash, i18n.site.deleteFromHere, () => ctrl.deleteNode(opts.path)),
action(
licon.Trash,
i18n.site.deleteFromHere,
() => ctrl.deleteNode(opts.path),
() => ctrl.deletionHighlightFromHere(opts.path, false),
() => ctrl.deletionHighlightFromHere(opts.path, true),
),

action(licon.PlusButton, i18n.site.expandVariations, () => ctrl.setAllCollapsed(opts.path, false)),

Expand All @@ -89,10 +127,15 @@ function view(opts: Opts, coords: Coords): VNode {
onMainline &&
action(licon.InternalArrow, i18n.site.forceVariation, () => ctrl.forceVariation(opts.path, true)),

action(licon.Clipboard, onMainline ? i18n.site.copyMainLinePgn : i18n.site.copyVariationPgn, () =>
navigator.clipboard.writeText(
renderVariationPgn(opts.root.data.game, opts.root.tree.getNodeList(opts.path), onMainline),
),
action(
licon.Clipboard,
onMainline ? i18n.site.copyMainLinePgn : i18n.site.copyVariationPgn,
() =>
navigator.clipboard.writeText(
renderVariationPgn(opts.root.data.game, opts.root.tree.getNodeList(extendedPath)),
),
() => ctrl.copyVariationHighlight(extendedPath, false),
() => ctrl.copyVariationHighlight(extendedPath, true),
),
],
);
Expand Down
6 changes: 6 additions & 0 deletions ui/lib/css/tree/_tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@
}
}
}
&.pendingDeletion {
background-color: rgba(255, 0, 13, 0.6) !important;
}
&.pendingCopy {
background-color: rgba(0, 255, 47, 0.6) !important;
}
}

&-column move {
Expand Down
20 changes: 20 additions & 0 deletions ui/lib/src/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface TreeWrapper {
pathIsMainline(path: Tree.Path): boolean;
pathIsForcedVariation(path: Tree.Path): boolean;
lastMainlineNode(path: Tree.Path): Tree.Node;
extendPath(path: Tree.Path, isMainline: boolean): Tree.Path;
pathExists(path: Tree.Path): boolean;
deleteNodeAt(path: Tree.Path): void;
setCollapsedAt(path: Tree.Path, collapsed: boolean): MaybeNode;
Expand All @@ -35,6 +36,7 @@ export interface TreeWrapper {
removeCeval(): void;
parentNode(path: Tree.Path): Tree.Node;
getParentClock(node: Tree.Node, path: Tree.Path): Tree.Clock | undefined;
getPathsOfDescendants(node: Tree.Node, path: Tree.Path): Tree.Path[];
}

export function build(root: Tree.Node): TreeWrapper {
Expand Down Expand Up @@ -102,6 +104,13 @@ export function build(root: Tree.Node): TreeWrapper {
return ops.childById(node, id);
});

const extendPath = (path: Tree.Path, isMainline: boolean): Tree.Path => {
let currNode = nodeAtPath(path);
while ((currNode = currNode?.children[0]) && !(isMainline && currNode.forceVariation))
path += currNode.id;
return path;
};

function updateAt(path: Tree.Path, update: (node: Tree.Node) => void): Tree.Node | undefined {
const node = nodeAtPathOrNull(path);
if (node) {
Expand Down Expand Up @@ -194,6 +203,15 @@ export function build(root: Tree.Node): TreeWrapper {
return parent ? parent.clock : node.clock;
}

function getPathsOfDescendants(node: Tree.Node, path: Tree.Path): Tree.Path[] {
const paths = [];
for (const child of node.children) {
const newPath = path + child.id;
paths.push(newPath, ...getPathsOfDescendants(child, newPath));
}
return paths;
}

return {
root,
lastPly: (): number => lastNode()?.ply || root.ply,
Expand Down Expand Up @@ -234,6 +252,7 @@ export function build(root: Tree.Node): TreeWrapper {
pathIsMainline,
pathIsForcedVariation,
lastMainlineNode: (path: Tree.Path): Tree.Node => lastMainlineNodeFrom(root, path),
extendPath,
pathExists,
deleteNodeAt,
promoteAt,
Expand All @@ -254,5 +273,6 @@ export function build(root: Tree.Node): TreeWrapper {
},
parentNode,
getParentClock,
getPathsOfDescendants,
};
}