lila/ui/analyse/src/treeView/treeView.ts

158 lines
4.5 KiB
TypeScript

import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { Hooks } from 'snabbdom/hooks'
import { game } from 'game';
import AnalyseCtrl from '../ctrl';
import contextMenu from './contextMenu';
import { MaybeVNodes, ConcealOf } from '../interfaces';
import { authorText as commentAuthorText } from '../study/studyComments';
import { enrichText, innerHTML } from '../util';
import { path as treePath } from 'tree';
import column from './columnView';
import inline from './inlineView';
import { empty, defined, dropThrottle, storedProp, StoredProp } from 'common';
export interface Ctx {
ctrl: AnalyseCtrl;
showComputer: boolean;
showGlyphs: boolean;
showEval: boolean;
truncateComments: boolean;
}
export interface Opts {
parentPath: Tree.Path;
isMainline: boolean;
inline?: Tree.Node;
withIndex?: boolean;
truncate?: number;
}
export interface NodeClasses {
active: boolean;
context_menu: boolean;
current: boolean;
nongame: boolean;
[key: string]: boolean;
}
export type TreeViewKey = 'column' | 'inline';
export interface TreeView {
get: StoredProp<TreeViewKey>;
set(inline: boolean): void;
toggle(): void;
inline(): boolean;
}
export function ctrl(): TreeView {
const value = storedProp<TreeViewKey>('treeView', 'column');
function inline() {
return value() === 'inline';
}
function set(i: boolean) {
value(i ? 'inline' : 'column');
}
return {
get: value,
set,
toggle() {
set(!inline());
},
inline
};
}
// entry point, dispatching to selected view
export function render(ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode {
return ctrl.treeView.inline() ? inline(ctrl) : column(ctrl, concealOf);
}
export function nodeClasses(c: AnalyseCtrl, path: Tree.Path): NodeClasses {
const current = (path === c.initialPath && game.playable(c.data)) || (
c.retro && c.retro.current() && c.retro.current().prev.path === path
);
return {
active: path === c.path,
context_menu: path === c.contextMenuPath,
current,
nongame: !current && !!c.gamePath && treePath.contains(path, c.gamePath) && path !== c.gamePath
};
}
export function renderInlineCommentsOf(ctx: Ctx, node: Tree.Node): MaybeVNodes {
if (!ctx.ctrl.showComments || empty(node.comments)) return [];
return node.comments!.map(comment => {
if (comment.by === 'lichess' && !ctx.showComputer) return;
const by = node.comments![1] ? `<span class="by">${commentAuthorText(comment.by)}</span>` : '',
truncated = truncateComment(comment.text, 300, ctx);
return h('comment', {
hook: innerHTML(truncated, text => by + enrichText(text, true))
});
}).filter(nonEmpty);
}
export function truncateComment(text: string, len: number, ctx: Ctx) {
return ctx.truncateComments && text.length > len ? text.slice(0, len - 10) + ' [...]' : text;
}
export function mainHook(ctrl: AnalyseCtrl): Hooks {
return {
insert: vnode => {
const el = vnode.elm as HTMLElement;
if (ctrl.path !== '') autoScroll(ctrl, el);
el.oncontextmenu = (e: MouseEvent) => {
const path = eventPath(e);
if (path !== null) contextMenu(e, {
path,
root: ctrl
});
ctrl.redraw();
return false;
};
el.addEventListener('mousedown', (e: MouseEvent) => {
if (defined(e.button) && e.button !== 0) return; // only touch or left click
const path = eventPath(e);
if (path) ctrl.userJump(path);
ctrl.redraw();
});
},
postpatch: (_, vnode) => {
if (ctrl.autoScrollRequested) {
autoScroll(ctrl, vnode.elm as HTMLElement);
ctrl.autoScrollRequested = false;
}
}
};
}
export function retroLine(ctx: Ctx, node: Tree.Node, opts: Opts): VNode | undefined {
return node.comp && ctx.ctrl.retro && ctx.ctrl.retro.hideComputerLine(node, opts.parentPath) ?
h('line', 'Learn from this mistake') : undefined;
}
function eventPath(e: MouseEvent): Tree.Path | null {
return (e.target as HTMLElement).getAttribute('p') ||
((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('p');
}
const scrollThrottle = dropThrottle(200);
export function autoScroll(ctrl: AnalyseCtrl, el: HTMLElement): void {
scrollThrottle(() => {
const cont = el.parentNode as HTMLElement;
if (!cont) return;
const target = el.querySelector('.active') as HTMLElement;
if (!target) {
cont.scrollTop = ctrl.path ? 99999 : 0;
return;
}
cont.scrollTop = target.offsetTop - cont.offsetHeight / 2 + target.offsetHeight;
});
}
export function nonEmpty(x: any): boolean {
return !!x;
}