diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6290cd1..43a04d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ First release.
> This section will be removed after the beta phase.
> 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.
- v0.12.0: Add `deep`, `resetLazy`, and `collapseOthers` options to
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 8a44358..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;
@@ -607,6 +621,13 @@ export enum NavModeEnum {
row = "row",
}
+/** */
+export type TranslationsType = {
+ loading: "Loading...";
+ loadError: "Error";
+ noData: "No data";
+ queryResult: "Matched ${match} of ${count} nodes.";
+};
/* -----------------------------------------------------------------------------
* METHOD OPTIONS TYPES
* ---------------------------------------------------------------------------*/
@@ -897,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.
*
@@ -908,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 52348f4..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;
}
@@ -252,6 +285,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_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 501e802..e429a87 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 ${count} nodes.",
* }
* ```
*/
- strings?: any; //[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 27eb445..11e8ebb 100644
--- a/src/wunderbaum.ts
+++ b/src/wunderbaum.ts
@@ -34,6 +34,7 @@ import {
FilterModeType,
FilterNodesOptions,
MatcherCallback,
+ NavigationType,
NavModeEnum,
NodeFilterCallback,
NodeRegion,
@@ -227,6 +228,7 @@ export class Wunderbaum {
loading: "Loading...",
// loading: "Loading…",
noData: "No data",
+ queryResult: "Matched ${match} of ${count} nodes.",
},
},
options
@@ -972,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);
@@ -1297,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(
@@ -1384,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
+}