@jinntec/fore
Version:
Fore - declarative user interfaces in plain HTML
445 lines (381 loc) • 12.9 kB
JavaScript
/**
* A simple collapsible treeview for showing JSON data.
*
*/
class FxJsonInstance extends HTMLElement {
// constructor(container, options = {}) {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
this.instanceElement = null;
this.foreSelector = null;
}
connectedCallback() {
this.container = this.querySelector('.json-path-picker-container');
this.foreSelector = this.hasAttribute('fore') ? this.getAttribute('fore') : 'fx-fore'; // default to first one in doc
this.render();
}
render() {
const style = `
@import '../../resources/fore.css';
:host {
display:block;
font-size:0.8em;
background:rgba(250, 250, 250, 0.9);
}
.container{
margin-left:1em;
}
.header{
margin-left:0;
}
::slot[name='header']{
margin-left:-1em;
}
/* Syntax highlighting for JSON objects */
ul.json-dict, ol.json-array {
list-style-type: none;
margin: 0 0 0 1px;
border-left: 1px dotted #ccc;
padding-left: 2em;
}
.json-string {
// color: #0B7500;
}
.json-literal {
color: #1A01CC;
font-weight: bold;
}
/* Toggle button */
a.json-toggle {
position: relative;
color: inherit;
text-decoration: none;
}
a.json-toggle:focus {
outline: none;
}
a.json-toggle:before {
content: "\\25BC"; /* down arrow */
position: absolute;
display: inline-block;
width: 1em;
left: -1.2em;
font-size:0.8em;
}
a.json-toggle.collapsed:before {
content: "\\25B6"; /* left arrow */
}
/* Collapsable placeholder links */
a.json-placeholder {
color: #aaa;
padding: 0 1em;
text-decoration: none;
}
a.json-placeholder:hover {
text-decoration: underline;
}
/* Copy path icon */
.pick-path {
color: lightgray;
cursor: pointer;
margin-left: 3px;
}
.pick-path:hover {
color: darkgray;
}
`;
const instanceId = this.hasAttribute('instance') ? this.getAttribute('instance') : 'default';
const fore = document.querySelector(this.foreSelector);
if (!fore) {
throw new Error(`this '${this.foreSelector}' does not match a fx-fore element`);
}
const html = `
<div class="container"></div>
`;
this.shadowRoot.innerHTML = `
<style>
${style}
</style>
<slot name="header">
<header class="header">${instanceId}</header>
</slot>
<slot></slot>
${html}
`;
// fore.addEventListener('ready', e => {
const instanceElement = document.querySelector(`#${instanceId}`);
if (
!instanceElement ||
instanceElement.nodeName !== 'FX-INSTANCE' ||
instanceElement.getAttribute('type') !== 'json'
) {
throw new Error(
`this '${instanceId}' does not match an fx-instance element or is not of type JSON`,
);
}
const container = this.shadowRoot.querySelector('.container');
const json = instanceElement.instanceData;
let tree = this.json2html(json, { outputWithQuotes: true });
if (this.isCollapsable(json)) tree = '<a href=\'#\' class="json-toggle"></a>'.concat(tree); // Insert HTML in target DOM element
container.innerHTML = tree;
const toggles = this.shadowRoot.querySelectorAll('.json-toggle');
toggles.forEach(toggle => {
toggle.addEventListener('click', this._handleToggleEvent.bind(this));
});
// container.addEventListener('click', (event) => this._handleToggleEvent);
// });
}
disconnectedCallback() {}
_isHidden(elem) {
const width = elem.offsetWidth;
const height = elem.offsetHeight;
return (width === 0 && height === 0) || window.getComputedStyle(elem).display === 'none';
}
_handleToggleEvent(event) {
// Change class
// event.preventDefault();
// event.stopPropagation();
const elm = event.target;
elm.classList.toggle('collapsed'); // Fetch every json-dict and json-array to toggle them
const subTarget = this._siblings(elm, 'ul.json-dict, ol.json-array', el => {
el.style.display = el.style.display === '' || el.style.display === 'block' ? 'none' : 'block';
}); // ForEach subtarget, previous siblings return array so we parse it
for (let i = 0; i < subTarget.length; i += 1) {
if (!this._isHidden(subTarget[i])) {
// Parse every siblings with '.json-placehoder' and remove them (previous add by else)
this._siblings(subTarget[i], '.json-placeholder', el => el.parentNode.removeChild(el));
} else {
// count item in object / array
const childs = subTarget[i].children;
let count = 0;
for (let j = 0; j < childs.length; j += 1) {
if (childs[j].tagName === 'LI') {
count += 1;
}
}
const placeholder = count + (count > 1 ? ' items' : ' item'); // Append a placeholder
subTarget[i].insertAdjacentHTML(
'afterend',
'<a href class="json-placeholder">'.concat(placeholder, '</a>'),
);
}
} // Prevent propagation
event.stopPropagation();
event.preventDefault();
}
_siblings(el, sel, callback) {
const sibs = [];
for (let i = 0; i < el.parentNode.children.length; i += 1) {
const child = el.parentNode.children[i];
if (child !== el && typeof sel === 'string' && child.matches(sel)) {
sibs.push(child);
}
} // If a callback is passed, call it on each sibs
if (callback && typeof callback === 'function') {
for (let _i = 0; _i < sibs.length; _i += 1) {
callback(sibs[_i]);
}
}
return sibs;
}
json2html(json, options) {
let html = '';
if (typeof json === 'string') {
// Escape tags
const tmp = json
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
if (this.isUrl(tmp)) {
html += '<a href="'.concat(tmp, '" class="json-string">').concat(tmp, '</a>');
} else {
html += '<span class="json-string">"'.concat(tmp, '"</span>');
}
} else if (typeof json === 'number') {
html += '<span class="json-literal">'.concat(json, '</span>');
} else if (typeof json === 'boolean') {
html += '<span class="json-literal">'.concat(json, '</span>');
} else if (json === null) {
html += '<span class="json-literal">null</span>';
} else if (json instanceof Array) {
if (json.length > 0) {
html += '[<ol class="json-array">';
for (let i = 0; i < json.length; i += 1) {
html += '<li data-key-type="array" data-key="'.concat(i, '">'); // Add toggle button if item is collapsable
if (this.isCollapsable(json[i])) {
html += '<a href="#" class="json-toggle"></a>';
}
html += this.json2html(json[i], options); // Add comma if item is not last
if (i < json.length - 1) {
html += ',';
}
html += '</li>';
}
html += '</ol>]';
} else {
html += '[]';
}
} else if (this._typeof(json) === 'object') {
let keyCount = Object.keys(json).length;
if (keyCount > 0) {
html += '{<ul class="json-dict">';
for (const key in json) {
if (json.hasOwnProperty(key)) {
html += '<li data-key-type="object" data-key="'.concat(key, '">');
const keyRepr = options.outputWithQuotes
? '<span class="json-string">"'.concat(key, '"</span>')
: key; // Add toggle button if item is collapsable
if (this.isCollapsable(json[key])) {
html += '<a href=\'#\' class="json-toggle">'.concat(keyRepr, '</a>');
} else {
html += keyRepr;
}
// ### keep the following comment for later - pick path is a good idea but needs to be adapted to XPath syntax
// html += '<span class="pick-path" title="Pick path">⧉</span>';
html += ': '.concat(this.json2html(json[key], options)); // Add comma if item is not last
keyCount -= 1;
if (keyCount > 0) {
html += ',';
}
html += '</li>';
}
}
html += '</ul>}';
} else {
html += '{}';
}
}
return html;
}
isUrl(string) {
const regexp =
/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#:.?+=&%@!\-/]))?/;
return regexp.test(string);
}
_typeof(obj) {
if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
this._typeof = function _typeof(obj) {
return typeof obj;
};
} else {
this._typeof = function _typeof(obj) {
return obj &&
typeof Symbol === 'function' &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? 'symbol'
: typeof obj;
};
}
return this._typeof(obj);
}
isCollapsable(arg) {
return arg instanceof Object && Object.keys(arg).length > 0;
}
/*
setup() {
// Create shadow DOM
// Add styles to shadow DOM
const style = document.createElement('style');
style.textContent = `
/!* add your CSS styles here *!/
`;
shadowRoot.appendChild(style);
// Move content to shadow DOM
const container = this.container.cloneNode(true);
shadowRoot.appendChild(container);
this.container.remove();
this.container = shadowRoot.querySelector('.json-path-picker-container');
this.clearBtn = shadowRoot.querySelector('.json-path-picker-clear-btn');
this.jsonTextarea = shadowRoot.querySelector('.json-path-picker-json');
this.treeView = shadowRoot.querySelector('.json-path-picker-tree');
this.resultView = shadowRoot.querySelector('.json-path-picker-result');
const data = {
"automobiles": [
{
"maker": "Nissan",
"model": "Teana",
"year": 2000
},
{
"maker": "Honda",
"model": "Jazz",
"year": 2023
},
{
"maker": "Honda",
"model": "Civic",
"year": 2007
},
{
"maker": "Toyota",
"model": "Yaris",
"year": 2008
},
{
"maker": "Honda",
"model": "Accord",
"year": 2011
}
],
"motorcycles": [{
"maker": "Honda",
"model": "ST1300",
"year": 2012
}]
}
this.updateTree(JSON.stringify(data));
}
*/
static get observedAttributes() {
return ['data'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'data') {
this.jsonTextarea.value = newValue;
this.updateTree(newValue);
}
}
updateTree(jsonString) {
try {
this.data = JSON.parse(jsonString);
this.treeView.innerHTML = '';
this.treeView.appendChild(this.createTreeView(this.data, ''));
} catch (e) {
console.error(e);
alert('Invalid JSON');
}
}
createTreeView(data, path) {
const ul = document.createElement('ul');
ul.classList.add('jp-ul');
if (Array.isArray(data)) {
data.forEach((item, index) => {
const li = document.createElement('li');
li.classList.add('jp-li');
const newPath = `${path}[${index}]`;
li.appendChild(this.createItemView(newPath, item));
ul.appendChild(li);
});
} else if (typeof data === 'object' && data !== null) {
Object.keys(data).forEach(key => {
const li = document.createElement('li');
li.classList.add('jp-li');
const newPath = `${path}.${key}`;
li.appendChild(this.createItemView(newPath, data[key]));
ul.appendChild(li);
});
} else {
const li = document.createElement('li');
li.classList.add('jp-li');
li.appendChild(this.createItemView(path, data));
ul.appendChild(li);
}
return ul;
}
}
if (!customElements.get('fx-json-instance')) {
customElements.define('fx-json-instance', FxJsonInstance);
}