Modernize & simplify treeview component

pull/364/head
Aarni Koskela 2023-03-30 19:10:15 +03:00
parent 51879dc714
commit 394523c5b7
3 changed files with 120 additions and 266 deletions

View File

@ -7,9 +7,11 @@ module.exports = {
plugins: ["prettier"],
rules: {
"class-methods-use-this": "off",
"max-classes-per-file": "off",
"no-console": "off",
"no-param-reassign": "off",
"prettier/prettier": "error",
curly: ["error", "all"],
"no-console": "off",
},
parserOptions: {
ecmaVersion: "latest",

View File

@ -1,4 +1,4 @@
/* global gradioApp, TreeView, get_uiCurrentTabContent, onUiUpdate, onUiLoaded */
/* global gradioApp, get_uiCurrentTabContent, onUiUpdate, onUiLoaded */
// prettier-ignore
const SDDP_HELP_TEXTS = {
"sddp-disable-negative-prompt": "Don't use prompt magic on negative prompts.",
@ -15,6 +15,121 @@ const SDDP_HELP_TEXTS = {
"sddp-write-raw-template": "Write template into image metadata.",
};
class SDDPTreeView {
/**
* @constructor
* @property {object} handlers The attached event handlers
* @property {object} data The JSON object that represents the tree structure
* @property {Element} node The DOM element to render the tree in
*/
constructor(data, node) {
this.handlers = {};
this.node = node;
this.data = data;
this.render();
}
/**
* Renders the tree view in the DOM
*/
render = () => {
const container = this.node;
container.innerHTML = "";
this.data.forEach((item) => container.appendChild(this.renderNode(item)));
[...container.querySelectorAll(".tree-leaf-text,.tree-expando")].forEach(
(node) => node.addEventListener("click", this.handleClickEvent),
);
};
renderNode = (item) => {
const leaf = document.createElement("div");
const content = document.createElement("div");
const text = document.createElement("div");
const expando = document.createElement("div");
leaf.setAttribute("class", "tree-leaf");
content.setAttribute("class", "tree-leaf-content");
text.setAttribute("class", "tree-leaf-text");
const { children, name, expanded } = item;
text.textContent = name;
expando.setAttribute("class", `tree-expando ${expanded ? "expanded" : ""}`);
expando.textContent = expanded ? "-" : "+";
content.appendChild(expando);
content.appendChild(text);
leaf.appendChild(content);
if (children?.length > 0) {
const childrenDiv = document.createElement("div");
childrenDiv.setAttribute("class", "tree-child-leaves");
children.forEach((child) => {
childrenDiv.appendChild(this.renderNode(child));
});
if (!expanded) {
childrenDiv.classList.add("hidden");
}
leaf.appendChild(childrenDiv);
} else {
expando.classList.add("hidden");
content.setAttribute("data-item", JSON.stringify(item));
}
return leaf;
};
handleClickEvent = (event) => {
const parent = (event.target || event.currentTarget).parentNode;
const leaves = parent.parentNode.querySelector(".tree-child-leaves");
if (leaves) {
this.setSubtreeVisibility(
parent,
leaves,
leaves.classList.contains("hidden"),
);
} else {
this.emit("select", {
target: event,
data: JSON.parse(parent.getAttribute("data-item")),
});
}
};
/**
* Expands/collapses by the expando or the leaf text
* @param {Element} node The parent node that contains the leaves
* @param {Element} leaves The leaves wrapper element
* @param {boolean} visible Expand or collapse?
* @param {boolean} skipEmit Skip emitting the event?
*/
setSubtreeVisibility(node, leaves, visible, skipEmit = false) {
leaves.classList.toggle("hidden", !visible);
node.querySelector(".tree-expando").textContent = visible ? "+" : "-";
if (skipEmit) {
return;
}
this.emit(visible ? "expand" : "collapse", {
target: node,
leaves,
});
}
on(name, callback, context = null) {
const handlers = this.handlers[name] || [];
handlers.push({ callback, context });
this.handlers[name] = handlers;
}
off(name, callback) {
this.handlers[name] = (this.handlers[name] || []).filter(
(handle) => handle.callback !== callback,
);
}
emit(name, ...args) {
(this.handlers[name] || []).forEach((handle) => {
window.setTimeout(() => {
handle.callback.apply(handle.context, args);
}, 0);
});
}
}
class SDDP_UI {
constructor() {
this.helpTextsConfigured = false;
@ -86,7 +201,7 @@ class SDDP_UI {
if (!this.treeView) {
const treeDiv = gradioApp().querySelector("#sddp-wildcard-tree");
if (treeDiv) {
treeView = new TreeView(content, treeDiv);
treeView = new SDDPTreeView(content, treeDiv);
treeView.on("select", this.onSelectNode.bind(this), null);
this.treeView = treeView;
}

View File

@ -1,263 +0,0 @@
(function (define) {
'use strict';
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.TreeView = factory();
}
}(window, function () {
return (function () {
/** List of events supported by the tree view */
var events = [
'expand',
'expandAll',
'collapse',
'collapseAll',
'select'
];
/**
* A utilite function to check to see if something is a DOM object
* @param {object} Object to test
* @returns {boolean} If the object is a DOM object
*/
function isDOMElement(obj) {
try {
return obj instanceof HTMLElement;
} catch (e) {
// Some browsers don't support using the HTMLElement so some extra
// checks are needed.
return typeof obj === 'object' && obj.nodeType === 1 && typeof obj.style === 'object' && typeof obj.ownerDocument === 'object';
}
}
/**
* A forEach that will work with a NodeList and generic Arrays
* @param {array|NodeList} arr The array to iterate over
* @param {function} callback Function that executes for each element. First parameter is element, second is index
* @param {object} The context to execute callback with
*/
function forEach(arr, callback, scope) {
var i, len = arr.length;
for (i = 0; i < len; i += 1) {
callback.call(scope, arr[i], i);
}
}
/**
* Emit an event from the tree view
* @param {string} name The name of the event to emit
*/
function emit(instance, name) {
var args = [].slice.call(arguments, 2);
if (events.indexOf(name) > -1) {
if (instance.handlers[name] && instance.handlers[name] instanceof Array) {
forEach(instance.handlers[name], function (handle) {
window.setTimeout(function () {
handle.callback.apply(handle.context, args);
}, 0);
});
}
} else {
throw new Error(name + ' event cannot be found on TreeView.');
}
}
/**
* Renders the tree view in the DOM
*/
function render(self) {
var container = isDOMElement(self.node) ? self.node : document.getElementById(self.node);
var leaves = [], click;
var renderLeaf = function (item) {
var leaf = document.createElement('div');
var content = document.createElement('div');
var text = document.createElement('div');
var expando = document.createElement('div');
leaf.setAttribute('class', 'tree-leaf');
content.setAttribute('class', 'tree-leaf-content');
content.setAttribute('data-item', JSON.stringify(item));
text.setAttribute('class', 'tree-leaf-text');
text.textContent = item.name;
expando.setAttribute('class', 'tree-expando ' + (item.expanded ? 'expanded' : ''));
expando.textContent = item.expanded ? '-' : '+';
content.appendChild(expando);
content.appendChild(text);
leaf.appendChild(content);
if (item.children && item.children.length > 0) {
var children = document.createElement('div');
children.setAttribute('class', 'tree-child-leaves');
forEach(item.children, function (child) {
var childLeaf = renderLeaf(child);
children.appendChild(childLeaf);
});
if (!item.expanded) {
children.classList.add('hidden');
}
leaf.appendChild(children);
} else {
expando.classList.add('hidden');
}
return leaf;
};
forEach(self.data, function (item) {
leaves.push(renderLeaf.call(self, item));
});
container.innerHTML = leaves.map(function (leaf) {
return leaf.outerHTML;
}).join('');
click = function (e) {
var parent = (e.target || e.currentTarget).parentNode;
var data = JSON.parse(parent.getAttribute('data-item'));
var leaves = parent.parentNode.querySelector('.tree-child-leaves');
if (leaves) {
if (leaves.classList.contains('hidden')) {
self.expand(parent, leaves);
} else {
self.collapse(parent, leaves);
}
} else {
emit(self, 'select', {
target: e,
data: data
});
}
};
forEach(container.querySelectorAll('.tree-leaf-text'), function (node) {
node.onclick = click;
});
forEach(container.querySelectorAll('.tree-expando'), function (node) {
node.onclick = click;
});
}
/**
* @constructor
* @property {object} handlers The attached event handlers
* @property {object} data The JSON object that represents the tree structure
* @property {DOMElement} node The DOM element to render the tree in
*/
function TreeView(data, node) {
this.handlers = {};
this.node = node;
this.data = data;
render(this);
}
TreeView.prototype.render = function () {
render(this);
};
/**
* Expands a leaflet by the expando or the leaf text
* @param {DOMElement} node The parent node that contains the leaves
* @param {DOMElement} leaves The leaves wrapper element
*/
TreeView.prototype.expand = function (node, leaves, skipEmit) {
var expando = node.querySelector('.tree-expando');
expando.textContent = '-';
leaves.classList.remove('hidden');
if (skipEmit) { return; }
emit(this, 'expand', {
target: node,
leaves: leaves
});
};
TreeView.prototype.expandAll = function () {
var self = this;
var nodes = document.getElementById(self.node).querySelectorAll('.tree-expando');
forEach(nodes, function (node) {
var parent = node.parentNode;
var leaves = parent.parentNode.querySelector('.tree-child-leaves');
if (parent && leaves && parent.hasAttribute('data-item')) {
self.expand(parent, leaves, true);
}
});
emit(this, 'expandAll', {});
};
/**
* Collapses a leaflet by the expando or the leaf text
* @param {DOMElement} node The parent node that contains the leaves
* @param {DOMElement} leaves The leaves wrapper element
*/
TreeView.prototype.collapse = function (node, leaves, skipEmit) {
var expando = node.querySelector('.tree-expando');
expando.textContent = '+';
leaves.classList.add('hidden');
if (skipEmit) { return; }
emit(this, 'collapse', {
target: node,
leaves: leaves
});
};
/**
*/
TreeView.prototype.collapseAll = function () {
var self = this;
var nodes = document.getElementById(self.node).querySelectorAll('.tree-expando');
forEach(nodes, function (node) {
var parent = node.parentNode;
var leaves = parent.parentNode.querySelector('.tree-child-leaves');
if (parent && leaves && parent.hasAttribute('data-item')) {
self.collapse(parent, leaves, true);
}
});
emit(this, 'collapseAll', {});
};
/**
* Attach an event handler to the tree view
* @param {string} name Name of the event to attach
* @param {function} callback The callback to execute on the event
* @param {object} scope The context to call the callback with
*/
TreeView.prototype.on = function (name, callback, scope) {
if (events.indexOf(name) > -1) {
if (!this.handlers[name]) {
this.handlers[name] = [];
}
this.handlers[name].push({
callback: callback,
context: scope
});
} else {
throw new Error(name + ' is not supported by TreeView.');
}
};
/**
* Deattach an event handler from the tree view
* @param {string} name Name of the event to deattach
* @param {function} callback The function to deattach
*/
TreeView.prototype.off = function (name, callback) {
var index, found = false;
if (this.handlers[name] instanceof Array) {
this.handlers[name].forEach(function (handle, i) {
index = i;
if (handle.callback === callback && !found) {
found = true;
}
});
if (found) {
this.handlers[name].splice(index, 1);
}
}
};
return TreeView;
}());
}));
}(window.define));