From 33d9c5830c5ab99fb20e548edb9f1065a3b89ba2 Mon Sep 17 00:00:00 2001 From: Martin Wendt Date: Sat, 1 Mar 2025 14:42:08 +0100 Subject: [PATCH 1/3] WiP --- CHANGELOG.md | 2 ++ src/types.ts | 7 +++++++ src/wb_ext_filter.ts | 4 ++++ src/wb_options.ts | 4 +++- src/wunderbaum.ts | 1 + 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6290cd1..d8eddb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ First release. > This section will be removed after the beta phase.
> Note that semantic versioning rules are not strictly followed during this phase. +- v0.13.0: Add support for prev/next-match + - v0.12.1: Fix flat source format for positional args. - v0.12.0: Add `deep`, `resetLazy`, and `collapseOthers` options to diff --git a/src/types.ts b/src/types.ts index 8a44358..6e8f3e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -607,6 +607,13 @@ export enum NavModeEnum { row = "row", } +/** */ +export type TranslationsType = { + loading: "Loading..."; + loadError: "Error"; + noData: "No data"; + queryResult: "Matched ${match} of ${total} nodes."; +}; /* ----------------------------------------------------------------------------- * METHOD OPTIONS TYPES * ---------------------------------------------------------------------------*/ diff --git a/src/wb_ext_filter.ts b/src/wb_ext_filter.ts index 52348f4..b6fafb9 100644 --- a/src/wb_ext_filter.ts +++ b/src/wb_ext_filter.ts @@ -252,6 +252,10 @@ export class FilterExtension extends WunderbaumExtension { tree.logDebug( `Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.` ); + const info = treeOpts.strings?.queryResult + .replace("${match}", "" + count) //this.countMatches()) + .replace("${count}", "" + tree.count()); + tree.log(info); return count; } diff --git a/src/wb_options.ts b/src/wb_options.ts index 501e802..c187831 100644 --- a/src/wb_options.ts +++ b/src/wb_options.ts @@ -19,6 +19,7 @@ import { NavModeEnum, NodeTypeDefinitionMap, SelectModeType, + TranslationsType, WbActivateEventType, WbButtonClickEventType, WbCancelableEventResultType, @@ -119,10 +120,11 @@ export interface WunderbaumOptions { * loading: "Loading...", * loadError: "Error", * noData: "No data", + * queryResult: "Matched ${match} of ${total} nodes.", * } * ``` */ - strings?: any; //[key: string] string; + strings?: TranslationsType; //[key: string] string; /** * 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose * Default: 3 (4 in local debug environment) diff --git a/src/wunderbaum.ts b/src/wunderbaum.ts index 27eb445..ead0908 100644 --- a/src/wunderbaum.ts +++ b/src/wunderbaum.ts @@ -227,6 +227,7 @@ export class Wunderbaum { loading: "Loading...", // loading: "Loading…", noData: "No data", + queryResult: "Matched ${match} of ${total} nodes.", }, }, options From 9751272653c962c608e0cddfd8474ba57b89ef6f Mon Sep 17 00:00:00 2001 From: Martin Wendt Date: Sun, 2 Mar 2025 11:45:07 +0100 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8eddb0..1f5c373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ First release. > This section will be removed after the beta phase.
> Note that semantic versioning rules are not strictly followed during this phase. -- v0.13.0: Add support for prev/next-match +- feature/next-match: Filter: Add support for prev/next-match - v0.12.1: Fix flat source format for positional args. From cfc601a1d863f392b27d72dd58e735baea34ffc2 Mon Sep 17 00:00:00 2001 From: Martin Wendt Date: Sun, 2 Mar 2025 21:12:18 +0100 Subject: [PATCH 3/3] WiP --- CHANGELOG.md | 1 + src/common.ts | 27 ++++++++++++++++++++-- src/types.ts | 51 ++++++++++++++++++++++++++++++---------- src/util.ts | 13 ----------- src/wb_ext_filter.ts | 55 +++++++++++++++++++++++++++++++++++--------- src/wb_node.ts | 17 +++++++------- src/wb_options.ts | 4 ++-- src/wunderbaum.ts | 21 +++++++++++++++-- tsconfig.json | 8 +++---- 9 files changed, 143 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f5c373..43a04d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ First release. > Note that semantic versioning rules are not strictly followed during this phase. - feature/next-match: Filter: Add support for prev/next-match +- feature/next-match: Filter: New mode 'mark' (like 'dim' but does not gray out) - v0.12.1: Fix flat source format for positional args. diff --git a/src/common.ts b/src/common.ts index 8bbc34b..4b5349f 100644 --- a/src/common.ts +++ b/src/common.ts @@ -4,7 +4,13 @@ * @VERSION, @DATE (https://github.com/mar10/wunderbaum) */ -import { MatcherCallback, SourceListType, SourceObjectType } from "./types"; +import { + ApplyCommandType, + MatcherCallback, + NavigationType, + SourceListType, + SourceObjectType, +} from "./types"; import * as util from "./util"; import { WunderbaumNode } from "./wb_node"; @@ -140,7 +146,24 @@ export const RESERVED_TREE_SOURCE_KEYS: Set = new Set([ // ]); /** Map `KeyEvent.key` to navigation action. */ -export const KEY_TO_ACTION_DICT: { [key: string]: string } = { +export const KEY_TO_NAVIGATION_MAP: { [key: string]: NavigationType } = { + ArrowDown: "down", + ArrowLeft: "left", + ArrowRight: "right", + ArrowUp: "up", + Backspace: "parent", + End: "lastCol", + Home: "firstCol", + "Control+End": "last", + "Control+Home": "first", + "Meta+ArrowDown": "last", // macOs + "Meta+ArrowUp": "first", // macOs + PageDown: "pageDown", + PageUp: "pageUp", +}; + +/** Map `KeyEvent.key` to navigation action. */ +export const KEY_TO_COMMAND_MAP: { [key: string]: ApplyCommandType } = { " ": "toggleSelect", "+": "expand", Add: "expand", diff --git a/src/types.ts b/src/types.ts index 6e8f3e4..b21c10a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -518,29 +518,43 @@ export interface WbEventInfo { // export type WbNodeCallbackType = (e: WbNodeEventType) => any; // export type WbRenderCallbackType = (e: WbRenderEventType) => void; -export type FilterModeType = null | "dim" | "hide"; +export type FilterModeType = null | "mark" | "dim" | "hide"; export type SelectModeType = "single" | "multi" | "hier"; + +export type NavigationType = + | "down" + | "first" + | "firstCol" + | "last" + | "lastCol" + | "left" + | "nextMatch" + | "pageDown" + | "pageUp" + | "parent" + | "prevMatch" + | "right" + | "up"; + export type ApplyCommandType = + | NavigationType | "addChild" | "addSibling" + | "collapse" + | "collapseAll" | "copy" | "cut" - | "down" - | "first" + | "edit" + | "expand" + | "expandAll" | "indent" - | "last" - | "left" | "moveDown" | "moveUp" | "outdent" - | "pageDown" - | "pageUp" - | "parent" | "paste" | "remove" | "rename" - | "right" - | "up"; + | "toggleSelect"; export type NodeFilterResponse = "skip" | "branch" | boolean | void; export type NodeFilterCallback = (node: WunderbaumNode) => NodeFilterResponse; @@ -612,7 +626,7 @@ export type TranslationsType = { loading: "Loading..."; loadError: "Error"; noData: "No data"; - queryResult: "Matched ${match} of ${total} nodes."; + queryResult: "Matched ${match} of ${count} nodes."; }; /* ----------------------------------------------------------------------------- * METHOD OPTIONS TYPES @@ -904,6 +918,19 @@ export interface VisitRowsOptions { /* ----------------------------------------------------------------------------- * wb_ext_filter * ---------------------------------------------------------------------------*/ + +/** + * Passed as tree option.filer.connect to configure automatic integration of + * filter UI controls. + */ +export interface FilterConnectType { + inputElem: string | HTMLInputElement | null; + modeButton?: string | HTMLButtonElement | HTMLAnchorElement | null; + nextButton?: string | HTMLButtonElement | HTMLAnchorElement | null; + prevButton?: string | HTMLButtonElement | HTMLAnchorElement | null; + matchInfoElem?: string | HTMLElement | null; +} + /** * Passed as tree options to configure default filtering behavior. * @@ -915,7 +942,7 @@ export type FilterOptionsType = { * Element or selector of an input control for filter query strings * @default null */ - connectInput?: null | string | Element; + connect?: null | FilterConnectType; /** * Re-apply last filter if lazy data is loaded * @default true diff --git a/src/util.ts b/src/util.ts index d4f85a0..6e01386 100644 --- a/src/util.ts +++ b/src/util.ts @@ -424,19 +424,6 @@ export function elemFromSelector(obj: string | T): T | null { return obj as T; } -// /** Return a EventTarget from selector or cast an existing element. */ -// export function eventTargetFromSelector( -// obj: string | EventTarget -// ): EventTarget | null { -// if (!obj) { -// return null; -// } -// if (typeof obj === "string") { -// return document.querySelector(obj) as EventTarget; -// } -// return obj as EventTarget; -// } - /** * Return a canonical descriptive string for a keyboard or mouse event. * diff --git a/src/wb_ext_filter.ts b/src/wb_ext_filter.ts index b6fafb9..f00715f 100644 --- a/src/wb_ext_filter.ts +++ b/src/wb_ext_filter.ts @@ -13,6 +13,7 @@ import { onEvent, } from "./util"; import { + FilterConnectType, FilterNodesOptions, FilterOptionsType, NodeFilterCallback, @@ -29,7 +30,11 @@ const RE_START_MARKER = new RegExp(escapeRegex(START_MARKER), "g"); const RE_END_MARTKER = new RegExp(escapeRegex(END_MARKER), "g"); export class FilterExtension extends WunderbaumExtension { - public queryInput?: HTMLInputElement; + public queryInput: HTMLInputElement | null = null; + public prevButton: HTMLElement | null = null; + public nextButton: HTMLButtonElement | HTMLAnchorElement | null = null; + public modeButton: HTMLButtonElement | HTMLAnchorElement | null = null; + public matchInfoElem: HTMLElement | null = null; public lastFilterArgs: IArguments | null = null; constructor(tree: Wunderbaum) { @@ -37,7 +42,7 @@ export class FilterExtension extends WunderbaumExtension { autoApply: true, // Re-apply last filter if lazy data is loaded autoExpand: false, // Expand all branches that contain matches while filtered matchBranch: false, // Whether to implicitly match all children of matched nodes - connectInput: null, // Element or selector of an input control for filter query strings + connect: null, // Element or selector of an input control for filter query strings fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar' hideExpanders: false, // Hide expanders if all child nodes are hidden by filter highlight: true, // Highlight matches by wrapping inside tags @@ -49,18 +54,45 @@ export class FilterExtension extends WunderbaumExtension { init() { super.init(); - const connectInput = this.getPluginOption("connectInput"); - if (connectInput) { - this.queryInput = elemFromSelector(connectInput) as HTMLInputElement; - assert( - this.queryInput, - `Invalid 'filter.connectInput' option: ${connectInput}.` - ); + const tree = this.tree; + const connect: FilterConnectType = this.getPluginOption("connect"); + if (connect) { + this.queryInput = elemFromSelector(connect.inputElem); + if (!this.queryInput) { + throw new Error( + `Invalid 'filter.connect' option: ${connect.inputElem}.` + ); + } + this.prevButton = elemFromSelector(connect.prevButton!); + this.nextButton = elemFromSelector(connect.nextButton!); + this.modeButton = elemFromSelector(connect.modeButton!); + this.matchInfoElem = elemFromSelector(connect.matchInfoElem!); + if (this.prevButton) { + onEvent(this.prevButton, "click", () => { + tree.findRelatedNode( + tree.getActiveNode() || tree.getFirstChild()!, + "prevMatch" + ); + }); + } + if (this.nextButton) { + onEvent(this.nextButton, "click", () => { + tree.findRelatedNode( + tree.getActiveNode() || tree.getFirstChild()!, + "nextMatch" + ); + }); + } + if (this.modeButton) { + onEvent(this.modeButton, "click", () => { + throw new Error("Not implemented"); + }); + } onEvent( this.queryInput, "input", debounce((e) => { - // this.tree.log("query", e); + // tree.log("query", e); this.filterNodes(this.queryInput!.value.trim(), {}); }, 700) ); @@ -72,7 +104,8 @@ export class FilterExtension extends WunderbaumExtension { super.setPluginOption(name, value); switch (name) { case "mode": - this.tree.filterMode = value === "hide" ? "hide" : "dim"; + this.tree.filterMode = + value === "hide" ? "hide" : value === "mark" ? "mark" : "dim"; this.tree.updateFilter(); break; } diff --git a/src/wb_node.ts b/src/wb_node.ts index c9792aa..6d56f0f 100644 --- a/src/wb_node.ts +++ b/src/wb_node.ts @@ -21,6 +21,7 @@ import { MakeVisibleOptions, MatcherCallback, NavigateOptions, + NavigationType, NodeAnyCallback, NodeStatusType, NodeStringCallback, @@ -46,7 +47,7 @@ import { import { decompressSourceData, ICON_WIDTH, - KEY_TO_ACTION_DICT, + KEY_TO_NAVIGATION_MAP, makeNodeTitleMatcher, nodeTitleSorter, RESERVED_TREE_SOURCE_KEYS, @@ -656,7 +657,7 @@ export class WunderbaumNode { * * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()} */ - findRelatedNode(where: string, includeHidden = false) { + findRelatedNode(where: NavigationType, includeHidden = false) { return this.tree.findRelatedNode(this, where, includeHidden); } @@ -1509,12 +1510,12 @@ export class WunderbaumNode { * e.g. `ArrowLeft` = 'left'. * @param options */ - async navigate(where: string, options?: NavigateOptions) { + async navigate(where: NavigationType | string, options?: NavigateOptions) { // Allow to pass 'ArrowLeft' instead of 'left' - where = KEY_TO_ACTION_DICT[where] || where; + const navType = (KEY_TO_NAVIGATION_MAP[where] ?? where) as NavigationType; // Otherwise activate or focus the related node - const node = this.findRelatedNode(where); + const node = this.findRelatedNode(navType); if (!node) { this.logWarn(`Could not find related node '${where}'.`); return Promise.resolve(this); @@ -2664,7 +2665,7 @@ export class WunderbaumNode { _setStatusNode({ statusNodeType: status, title: - tree.options.strings.loading + + tree.options.strings!.loading + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, @@ -2677,7 +2678,7 @@ export class WunderbaumNode { _setStatusNode({ statusNodeType: status, title: - tree.options.strings.loadError + + tree.options.strings!.loadError + (message ? " (" + message + ")" : ""), checkbox: false, colspan: true, @@ -2690,7 +2691,7 @@ export class WunderbaumNode { case "noData": _setStatusNode({ statusNodeType: status, - title: message || tree.options.strings.noData, + title: message || tree.options.strings!.noData, checkbox: false, colspan: true, tooltip: details, diff --git a/src/wb_options.ts b/src/wb_options.ts index c187831..e429a87 100644 --- a/src/wb_options.ts +++ b/src/wb_options.ts @@ -120,11 +120,11 @@ export interface WunderbaumOptions { * loading: "Loading...", * loadError: "Error", * noData: "No data", - * queryResult: "Matched ${match} of ${total} nodes.", + * queryResult: "Matched ${match} of ${count} nodes.", * } * ``` */ - strings?: TranslationsType; //[key: string] string; + strings?: TranslationsType; /** * 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose * Default: 3 (4 in local debug environment) diff --git a/src/wunderbaum.ts b/src/wunderbaum.ts index ead0908..11e8ebb 100644 --- a/src/wunderbaum.ts +++ b/src/wunderbaum.ts @@ -34,6 +34,7 @@ import { FilterModeType, FilterNodesOptions, MatcherCallback, + NavigationType, NavModeEnum, NodeFilterCallback, NodeRegion, @@ -227,7 +228,7 @@ export class Wunderbaum { loading: "Loading...", // loading: "Loading…", noData: "No data", - queryResult: "Matched ${match} of ${total} nodes.", + queryResult: "Matched ${match} of ${count} nodes.", }, }, options @@ -973,9 +974,11 @@ export class Wunderbaum { case "first": case "last": case "left": + case "nextMatch": case "pageDown": case "pageUp": case "parent": + case "prevMatch": case "right": case "up": return node.navigate(cmd); @@ -1298,7 +1301,11 @@ export class Wunderbaum { * e.g. `$.ui.keyCode.LEFT` = 'left'. * @param includeHidden Not yet implemented */ - findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) { + findRelatedNode( + node: WunderbaumNode, + where: NavigationType, + includeHidden = false + ) { const rowHeight = this.options.rowHeightPx!; let res = null; const pageSize = Math.floor( @@ -1385,6 +1392,16 @@ export class Wunderbaum { } } break; + + case "prevMatch": + // fallthrough + case "nextMatch": + if (!this.isFilterActive) { + this.logWarn(`${where}: Filter is not active.`); + break; + } + throw new Error("Not implemented"); + default: this.logWarn("Unknown relation '" + where + "'."); } diff --git a/tsconfig.json b/tsconfig.json index bded5c9..ab902ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,7 @@ "noUnusedLocals": true /* Report errors on unused locals. */, // "noUnusedParameters": true /* Report errors on unused parameters. */, // "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, - "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + // "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, /* Module Resolution Options */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ @@ -55,7 +55,7 @@ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ - "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "exclude": [ "node_modules", @@ -73,11 +73,11 @@ "src/wb_options.ts", "src/common.ts", "src/types.ts", - "src/util.ts", + "src/util.ts" ], "out": "docs/api", "excludePrivate": true, "excludeProtected": true, "includeVersion": true } -} \ No newline at end of file +}