Skip to content

Instantly share code, notes, and snippets.

@krukid
Last active August 13, 2020 21:57
Show Gist options
  • Save krukid/3a3ac1887a8534fc4d7be9455f0281ab to your computer and use it in GitHub Desktop.
Save krukid/3a3ac1887a8534fc4d7be9455f0281ab to your computer and use it in GitHub Desktop.
mint-object-explorer
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
function matchString(source, pattern, specifier) {
const match = pattern.match(/^(?:(\w+)=)?(")?([^"]+)\2$/);
if (match) {
const [, prefix, quote, text] = match;
const isPresent = text.trim();
const isRelevant = prefix === undefined || prefix === specifier;
const isWildcard = text === '*';
const isExact = quote !== undefined;
return isRelevant && isPresent && (
isWildcard || (
isExact
? source === text
: source.includes(text)
)
);
}
return false;
}
function matchNodeKey(node, filter) {
return matchString(node.key, filter, 'key');
}
function matchNodeValue(node, filter) {
return matchString(String(node.value), filter, 'value');
}
function nodeify(key, object) {
const type = Object.prototype.toString.apply(object).slice(8, -1);
return {
key,
type,
value: object,
isObject: type === 'Object',
isArray: type === 'Array',
isContainer: ['Object', 'Array'].includes(type),
};
}
function recursivelyNodeify(key, object, filter, strict) {
const node = nodeify(key, object);
// determine match by key
node.matchKey = matchNodeKey(node, filter);
// nodeify container children
if (node.isObject) {
node.children = Object.entries(object).map(([childKey, childObject]) => recursivelyNodeify(childKey, childObject, filter, strict));
}
else if (node.isArray) {
node.children = object.map((childObject, childKey) => recursivelyNodeify(String(childKey), childObject, filter, strict));
}
// filter container children if strict
if (node.isContainer && strict) {
node.children = node.children.filter(childNode => node.matchKey || childNode.matchKey || childNode.matchValue);
}
// determine match by value
if (node.isContainer) {
node.matchValue = node.children.findIndex(childNode => childNode.matchKey || childNode.matchValue) >= 0;
}
else {
node.matchValue = matchNodeValue(node, filter);
}
return node;
}
export default class extends Component {
@tracked
filter = '';
@tracked
isStrictFilter = false;
get tree() {
const tree = recursivelyNodeify('root', this.args.object, this.filter, this.isStrictFilter);
console.debug(tree);
return tree;
}
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class extends Component {
json = '';
@tracked
error = true;
@action
change(event) {
let object;
try {
object = JSON.parse(event.target.value);
this.error = null;
}
catch (e) {
console.debug(e);
this.error = e;
}
this.args.onParsed(object);
}
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class extends Component {
@tracked
isPreferExpanded = false;
get isExpanded() {
return this.isPreferExpanded || !this.args.node.isContainer || this.args.node.matchValue || this.args.node.matchKey;
}
get isHighlighted() {
return this.args.node.matchValue || this.args.node.matchKey;
}
get keyClassName() {
return this.isPreferExpanded && this.args.node.isContainer
? 'mint-object__key--expanded'
: 'mint-object__key--elastic';
}
get valueClassName() {
switch (this.args.node.type) {
case 'Number': return 'mint-object__value--number';
case 'String': return 'mint-object__value--string';
case 'Boolean': return 'mint-object__value--boolean';
case 'Object': return 'mint-object__value--object';
}
}
get rootClassName() {
return [
this.isExpanded
? 'mint-object--expanded'
: 'mint-object--collapsed',
this.isHighlighted
? 'mint-object--pronounced'
: 'mint-object--dimmed'
].join(' ');
}
get value() {
return String(this.args.node.value);
}
@action
expand() {
this.isPreferExpanded = !this.isPreferExpanded;
}
}
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
const DEMO_OBJECT = {
a: {
b: 'foo',
c: 1,
d: {},
e: {
w: 'qux wii',
},
},
i: -5.1,
b: false,
s: 'bar',
};
export default class ApplicationController extends Controller {
appName = 'mint-object-explorer';
@tracked
customObject;
get object() {
return this.customObject === undefined
? DEMO_OBJECT
: this.customObject;
}
@action
objectParsed(object) {
this.customObject = object;
}
}
potentially useful features/ tweaks:
1. persistent node toggle across searches
2. node toggles expand AND highlight (alt color?)
3. opacity darken bg for nested containers
4. search expressions (key="foo" value=2 AND value=5)
// key is exactly "foo" OR value contains both 2 and 5
// eg: `a.foo.b=352` matches a.foo (by key) and a.foo.b (by value)
5. extended expressions (key>5 [numeric] value<="qwe" [lexicographic])
// consider /, /= OR ->, => for lex >, >=
// \, \= OR <-, <= for lex <, <=
// key<=qwe value>5
6. prefixed list display instead of tree (filter leaf nodes)
// a.b.c: 5
// x.y: qwd
7. key path matching
// key=a > value=b > key="c" === match nodes with key containing "a" that have children with value containing "b", if those children have key "c"
// path=a.b.c === key=a > key=b > key=c
// path="a.b.c" === key="a" > key="b" > key="c"
?. css-like expressions VS search-like expressions?
body {
margin: 12px 16px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 12pt;
}
.mint-object {
}
.mint-object--dimmed {
color: silver;
}
.mint-object--pronounced {
color: black;
}
.mint-object__key {
font-weight: 700;
}
.mint-object__key--elastic {
}
.mint-object__key--expanded {
color: darkmagenta;
}
.mint-object__key[role=button]:hover {
text-decoration: underline;
color: magenta;
}
.mint-object__value {
padding-left: 15px;
}
.mint-object__value--object {
border-left: dashed 1px silver;
}
.mint-object--pronounced > .mint-object__value--string {
color: orange;
}
.mint-object--pronounced > .mint-object__value--boolean {
color: black;
}
.mint-object--pronounced > .mint-object__value--number {
color: blue;
}
<h1>{{this.appName}}</h1>
<p>display and filter arbitrary POJOs</p>
<MintObjectParser @onParsed={{this.objectParsed}} />
<MintObjectExplorer @object={{this.object}} />
<label>Search:</label>
<Input @value={{this.filter}} />
<Input @type="checkbox" @checked={{this.isStrictFilter}} />
Strict
<ol class="mint-object-explorer__hint">
<li>plain text will search substring matches in keys and values</li>
<li>double-quoted text will search for exact matches in keys and values</li>
<li>key/value prefix followed by equals sign will narrow search by type</li>
<li>to expand non-matching objects, click on its root key</li>
</ol>
<MintObject @node={{this.tree}} />
<div>
<label>Custom JSON:</label>
<Textarea @value={{this.json}} {{on "input" this.change}} />
</div>
<div>
<label>Custom JSON parse status:</label>
{{if this.error "FAIL" "OK"}}
</div>
<div class="mint-object {{this.rootClassName}}">
<div
class="mint-object__key {{this.keyClassName}}"
role={{if @node.isContainer "button"}}
{{on "click" this.expand}}
>
{{@node.key}}
</div>
{{if @node.isObject "{"}}
{{if @node.isArray "["}}
{{#if this.isExpanded}}
<div class="mint-object__value {{this.valueClassName}}">
{{#if @node.isContainer}}
{{#each @node.children as |childNode|}}
<MintObject @node={{childNode}} />
{{/each}}
{{else}}
{{this.value}}
{{/if}}
</div>
{{/if}}
{{if @node.isArray "]"}}
{{if @node.isObject "}"}}
</div>
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1"
},
"addons": {
"@glimmer/component": "1.0.0"
}
}
@krukid
Copy link
Author

krukid commented Aug 13, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment