diff --git a/app/components/component-tree-item.hbs b/app/components/component-tree-item.hbs
index 450b7ce0b4..8ebaf663d5 100644
--- a/app/components/component-tree-item.hbs
+++ b/app/components/component-tree-item.hbs
@@ -1,6 +1,7 @@
{{!-- template-lint-disable no-invalid-interactive --}}
- {{@item.name}}
+ {{mark-match @item.name @searchTerm}}
{{#each @item.args.positional as |value|}}
@@ -63,7 +64,7 @@
{{/each-in}}
{{else}}
- {{classify @item.name}}
+ {{mark-match (classify @item.name) @searchTerm}}
{{#each-in @item.args.named as |name value|}}
@@ -78,11 +79,11 @@
{{/each-in}}
{{/if}}
{{else if @item.isOutlet}}
- \{{outlet "{{@item.name}}"}}
+ \{{outlet "{{mark-match @item.name @searchTerm}}"}}
{{else if @item.isEngine}}
- \{{mount "{{@item.name}}"}}
+ \{{mount "{{mark-match @item.name @searchTerm}}"}}
{{else if @item.isRouteTemplate}}
- {{@item.name}} route
+ {{mark-match @item.name @searchTerm}} route
{{/if}}
diff --git a/app/controllers/component-tree.js b/app/controllers/component-tree.js
index 927beaae61..6a53287a19 100644
--- a/app/controllers/component-tree.js
+++ b/app/controllers/component-tree.js
@@ -19,10 +19,15 @@ export default class ComponentTreeController extends Controller {
@tracked query = '';
@tracked isInspecting = false;
+ /**
+ *
+ * @type {RenderItem[]}
+ */
@tracked renderItems = [];
@tracked _pinned = undefined;
@tracked _previewing = undefined;
+ @tracked searchSelect = undefined;
_store = Object.create(null);
@@ -30,6 +35,10 @@ export default class ComponentTreeController extends Controller {
let { _store } = this;
let store = Object.create(null);
+ /**
+ *
+ * @type {RenderItem[]}
+ */
let renderItems = [];
let flatten = (parent, renderNode) => {
@@ -80,6 +89,14 @@ export default class ComponentTreeController extends Controller {
}
get nextItem() {
+ if (this.searchSelect) {
+ const items = this.matchingItems;
+ const index = items.indexOf(this.findItem(this.pinned)) + 1;
+ return (
+ items.slice(index).find((i) => searchMatch(i.name, this.searchSelect)) ||
+ this.currentItem
+ );
+ }
const items = this.visibleItems;
return (
items[items.indexOf(this.findItem(this.pinned)) + 1] ||
@@ -88,6 +105,16 @@ export default class ComponentTreeController extends Controller {
}
get previousItem() {
+ if (this.searchSelect) {
+ const items = this.matchingItems;
+ const index = items.indexOf(this.findItem(this.pinned));
+ return (
+ items
+ .slice(0, index)
+ .reverse()
+ .find((i) => searchMatch(i.name, this.searchSelect)) || this.currentItem
+ );
+ }
const items = this.visibleItems;
return items[items.indexOf(this.findItem(this.pinned)) - 1] || items[0];
}
@@ -193,6 +220,10 @@ export default class ComponentTreeController extends Controller {
}
}
+ @action handleClick() {
+ this.searchSelect = false;
+ }
+
@action handleKeyDown(event) {
if (focusedInInput()) {
return;
@@ -204,13 +235,16 @@ export default class ComponentTreeController extends Controller {
switch (event.keyCode) {
case KEYS.up:
- this.pinned = this.previousItem.id;
+ this.pinned = this.previousItem?.id;
break;
case KEYS.right: {
+ if (this.searchSelect) {
+ break;
+ }
const pinnedItem = this.findItem(this.pinned);
if (pinnedItem.isExpanded) {
- this.pinned = this.nextItem.id;
+ this.pinned = this.nextItem?.id;
} else {
pinnedItem.expand();
}
@@ -218,9 +252,12 @@ export default class ComponentTreeController extends Controller {
break;
}
case KEYS.down:
- this.pinned = this.nextItem.id;
+ this.pinned = this.nextItem?.id;
break;
case KEYS.left: {
+ if (this.searchSelect) {
+ break;
+ }
const pinnedItem = this.findItem(this.pinned);
if (pinnedItem.isExpanded) {
@@ -231,6 +268,22 @@ export default class ComponentTreeController extends Controller {
break;
}
+ case KEYS.escape:
+ this.searchSelect = undefined;
+ break;
+ case KEYS.backspace:
+ this.searchSelect = (this.searchSelect || '').slice(0, -1);
+ break;
+ default:
+ if (event.key.length === 1) {
+ this.searchSelect = (this.searchSelect || '') + event.key;
+ if (
+ !this.currentItem ||
+ !searchMatch(this.currentItem.name, this.searchSelect)
+ ) {
+ this.pinned = this.nextItem?.id;
+ }
+ }
}
}
@@ -248,10 +301,12 @@ export default class ComponentTreeController extends Controller {
@action arrowKeysSetup() {
document.addEventListener('keydown', this.handleKeyDown);
+ document.addEventListener('click', this.handleClick);
}
@action arrowKeysTeardown() {
document.removeEventListener('keydown', this.handleKeyDown);
+ document.removeEventListener('click', this.handleClick);
}
}
diff --git a/app/helpers/mark-match.js b/app/helpers/mark-match.js
new file mode 100644
index 0000000000..8ffe908df9
--- /dev/null
+++ b/app/helpers/mark-match.js
@@ -0,0 +1,33 @@
+import { helper } from '@ember/component/helper';
+import { htmlSafe } from '@ember/template';
+import searchMatch from 'ember-inspector/utils/search-match';
+
+
+function replaceRange(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+}
+/**
+ *
+ * @param str {string}
+ * @param regex {RegExp}
+ * @return {SafeString}
+ */
+export function markMatch([str, regex]) {
+ if (!regex) {
+ return str;
+ }
+ const match = searchMatch(str, regex);
+ if (!match) {
+ return str;
+ }
+ const matchedText = str.slice(match.index, match.index + match[0].length);
+ str = replaceRange(
+ str,
+ match.index,
+ match.index + match[0].length,
+ `${matchedText}`
+ );
+ return htmlSafe(str);
+}
+
+export default helper(markMatch);
diff --git a/app/styles/component_tree.scss b/app/styles/component_tree.scss
index bb399c8e1f..f607380f2f 100644
--- a/app/styles/component_tree.scss
+++ b/app/styles/component_tree.scss
@@ -210,3 +210,14 @@
.component-tree-item--component {
color: var(--base09);
}
+
+.component-tree-search {
+ background-color: var(--base00);
+ border: 1px solid var(--base10);
+ box-shadow: 0 0 3px var(--base15);
+ padding: 3px;
+ position: absolute;
+ right: 20px;
+ top: 3px;
+ z-index: 1;
+}
diff --git a/app/templates/component-tree.hbs b/app/templates/component-tree.hbs
index a0944481d9..54cbe08194 100644
--- a/app/templates/component-tree.hbs
+++ b/app/templates/component-tree.hbs
@@ -10,6 +10,10 @@
{{/in-element}}
{{/if}}
+{{#if this.searchSelect}}
+ selecting: {{this.searchSelect}}
+{{/if}}
+
-
+
\ No newline at end of file
diff --git a/app/utils/key-codes.js b/app/utils/key-codes.js
index 4776758dc7..f664fd95a2 100644
--- a/app/utils/key-codes.js
+++ b/app/utils/key-codes.js
@@ -5,6 +5,7 @@ const KEYS = {
right: 39,
down: 40,
left: 37,
+ backspace: 8,
};
export { KEYS };
diff --git a/app/utils/search-match.js b/app/utils/search-match.js
index d578459548..407f10614f 100644
--- a/app/utils/search-match.js
+++ b/app/utils/search-match.js
@@ -10,5 +10,5 @@ export default function (text, searchQuery) {
return true;
}
let regExp = new RegExp(escapeRegExp(sanitize(searchQuery)));
- return !!sanitize(text).match(regExp);
+ return sanitize(text).match(regExp);
}
diff --git a/tests/acceptance/component-tree-test.js b/tests/acceptance/component-tree-test.js
index 80cbf1f3b6..66422d816c 100644
--- a/tests/acceptance/component-tree-test.js
+++ b/tests/acceptance/component-tree-test.js
@@ -13,6 +13,7 @@ import {
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupTestAdapter, respondWith, sendMessage } from '../test-adapter';
+import { KEYS } from 'ember-inspector/utils/key-codes';
function textFor(selector, context) {
return context.querySelector(selector).textContent.trim();
@@ -108,8 +109,8 @@ function getRenderTree({ withChildren, withManyChildren } = {}) {
const children = [];
if (withChildren) {
children.push(
- Component({ id: 5, name: 'sub-task' }),
- Component({ id: 6, name: 'sub-task' })
+ Component({ id: 5, name: 'sub-task-' + 5 }),
+ Component({ id: 6, name: 'sub-task-' + 6 })
);
}
if (withManyChildren) {
@@ -354,6 +355,97 @@ module('Component Tab', function (hooks) {
assert.strictEqual(treeNodes.length, 4, 'expected all tree nodes');
});
+ test('It should search select when typing characters', async function (assert) {
+ assert.expect(26);
+ await visit('/component-tree');
+
+ respondWith('view:showInspection', false, { count: 12 });
+
+ await sendMessage({
+ type: 'view:renderTree',
+ tree: getRenderTree({ withManyChildren: true }),
+ });
+
+ let expanders = findAll('.component-tree-item__expand');
+ let expanderEl = expanders[expanders.length - 1];
+ await click(expanderEl);
+
+ async function triggerCharCodes(text) {
+ for (let t of text) {
+ await triggerKeyEvent(document, 'keydown', t.toUpperCase());
+ }
+ }
+
+ let treeNodes = findAll('.component-tree-item');
+ assert.strictEqual(treeNodes.length, 32, 'expected all tree nodes');
+
+ respondWith('view:showInspection', false);
+ respondWith('objectInspector:inspectById', ({ objectId }) => {
+ const result = findAll(
+ `[data-test-id=${objectId}] .component-tree-item__tag`
+ )[0];
+ // matching initial character s
+ assert.strictEqual(
+ result.textContent.trim(),
+ 'todos route',
+ 'should first select todos route'
+ );
+ return false;
+ });
+
+ await triggerCharCodes('sub-task-1');
+ await rerender();
+
+ treeNodes = findAll('mark');
+ treeNodes.forEach((node) => {
+ assert.strictEqual(
+ node.textContent.trim(),
+ 'SubTask1',
+ 'SubTask1 text part should be marked'
+ );
+ });
+ assert.strictEqual(
+ treeNodes.length,
+ 10,
+ 'expected nodes with sub-task-1 name to be marked'
+ );
+
+ treeNodes = findAll('.component-tree-item');
+ assert.strictEqual(treeNodes.length, 32, 'expected all tree nodes');
+
+ respondWith('view:showInspection', false);
+
+ let subTaskNumber = 11;
+ for (let i = 0; i < 10; i++) {
+ await triggerKeyEvent(document, 'keydown', KEYS.down);
+ const node = findAll(
+ '.component-tree-item--pinned .component-tree-item__tag'
+ )[0];
+ assert.strictEqual(
+ node.textContent.trim(),
+ 'SubTask' + subTaskNumber,
+ 'should include SubTask1'
+ );
+ subTaskNumber++;
+ if (subTaskNumber === 20) {
+ subTaskNumber = 100;
+ }
+ }
+
+ await triggerKeyEvent(document, 'keydown', KEYS.up);
+ const node = findAll(
+ '.component-tree-item--pinned .component-tree-item__tag'
+ )[0];
+ assert.strictEqual(
+ node.textContent.trim(),
+ 'SubTask19',
+ 'should include SubTask1'
+ );
+
+ treeNodes = findAll('.component-tree-item');
+ assert.strictEqual(treeNodes.length, 33, 'expected all tree nodes');
+ });
+
test('It should update the view tree when the port triggers a change, preserving the expanded state of existing nodes', async function (assert) {
await visit('/component-tree');