Move utility functions to separate script file. Begin redesign of clusterizer.

This commit is contained in:
Sj-Si 2024-04-05 08:40:49 -04:00
parent 9c77d3fbe7
commit 4e31bca922
6 changed files with 796 additions and 257 deletions

489
javascript/clusterize.js Normal file
View file

@ -0,0 +1,489 @@
/* eslint-disable */
/*
Heavily modified Clusterize.js v1.0.0.
Original: http://NeXTs.github.com/Clusterize.js/
This has been modified to allow for an asynchronous data loader implementation.
This differs from the original Clusterize.js which would store the entire dataset
in an array and load from that; this caused a large memory overhead in the client.
*/
const SCROLL_DEBOUNCE_TIME_MS = 50;
const RESIZE_OBSERVER_DEBOUNCE_TIME_MS = 100;
const ELEMENT_OBSERVER_DEBOUNCE_TIME_MS = 100;
class Clusterize {
scroll_elem = null;
content_elem = null;
scroll_id = null;
content_id = null;
#options = {};
#is_mac = null;
#ie = null;
#n_rows = null;
#cache = {};
#scroll_top = 0;
#last_cluster = false;
#scroll_debounce = 0;
#resize_observer = null;
#resize_observer_timer = null;
#element_observer = null;
#element_observer_timer = null;
#pointer_events_set = false;
#sort_mode = "";
#sort_dir = "";
constructor(args) {
const defaults = {
rows_in_block: 50,
blocks_in_cluster: 4,
tag: null,
show_no_data_row: true,
no_data_class: 'clusterize-no-data',
no_data_text: 'No data',
keep_parity: true,
callbacks: {}
};
const options = [
'rows_in_block',
'blocks_in_cluster',
'show_no_data_row',
'no_data_class',
'no_data_text',
'keep_parity',
'tag',
'callbacks',
];
// detect ie9 and lower
// https://gist.github.com/padolsey/527683#comment-786682
this.#ie = (function () {
for (var v = 3,
el = document.createElement('b'),
all = el.all || [];
el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
all[0];
) { }
return v > 4 ? v : document.documentMode;
}())
this.#is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
for (let i = 0, option; option = options[i]; i++) {
this.#options[option] = !isNullOrUndefined(args[option]) ? args[option] : defaults[option];
}
this.scroll_elem = args["scrollId"] ? document.getElementById(args["scrollId"]) : args["scrollElem"];
if (!isElement(this.scroll_elem)) {
throw new Error("Error! Could not find scroll element");
}
this.scroll_id = this.scroll_elem.id;
this.content_elem = args["contentId"] ? document.getElementById(args["contentId"]) : args["contentElem"];
if (!isElement(this.content_elem)) {
throw new Error("Error! Could not find content element");
}
this.content_id = this.content_elem.id;
if (!this.content_elem.hasAttribute("tabindex")) {
this.content_elem.setAttribute("tabindex", 0);
}
this.#scroll_top = this.scroll_elem.scrollTop;
if (!isNumber(args.n_rows)) {
throw new Error("Invalid argument. n_rows expected number, got:", typeof args.n_rows);
}
this.#n_rows = args.n_rows;
if (!this.#options.callbacks.fetchData) {
this.#options.callbacks.fetchData = this.#fetchDataDefault;
}
if (!this.#options.callbacks.sortData) {
this.#options.callbacks.sortData = this.#sortDataDefault;
}
if (!this.#options.callbacks.filterData) {
this.#options.callbacks.filterData = this.#filterDataDefault;
}
}
// ==== PUBLIC FUNCTIONS ====
async setup() {
await this.#insertToDOM();
this.scroll_elem.scrollTop = this.#scroll_top;
this.#setupEvent("scroll", this.scroll_elem, this.#onScroll);
this.#setupElementObservers();
this.#setupResizeObservers();
}
destroy() {
this.#teardownEvent("scroll", this.scroll_elem, this.#onScroll);
this.#teardownElementObservers();
this.#teardownResizeObservers();
this.#html(this.#generateEmptyRow().join(""));
}
refresh(force) {
if (this.#getRowsHeight() || force) {
this.update()
}
}
async update() {
this.#scroll_top = this.scroll_elem.scrollTop;
// fixes #39
if (this.#n_rows * this.#options.item_height < this.#scroll_top) {
this.scroll_elem.scrollTop = 0;
this.#last_cluster = 0;
}
await this.#insertToDOM();
this.scroll_elem.scrollTop = this.#scroll_top;
}
getRowsAmount() {
return this.#n_rows;
}
getScrollProgress() {
return this.#options.scroll_top / (this.#n_rows * this.#options.item_height) * 100 || 0;
}
async filterData(filter) {
// Filter is applied to entire dataset.
const n_rows = await this.#options.callbacks.filterData(filter);
// If the number of rows changed after filter, we need to update the cluster.
if (n_rows !== this.#n_rows) {
this.#n_rows = n_rows;
this.refresh(true);
}
// Apply sort to the new filtered data.
await this.sortData(this.#sort_mode, this.#sort_dir);
}
async sortData(mode, dir) {
// Sort is applied to the filtered data.
// update instance sort settings to the passed values.
this.#sort_mode = mode;
this.#sort_dir = dir;
await this.#options.callbacks.sortData(this.#sort_mode, this.#sort_dir === "descending");
await this.#insertToDOM();
}
// ==== PRIVATE FUNCTIONS ====
#fetchDataDefault() {
return Promise.resolve([]);
}
#sortDataDefault() {
return Promise.resolve([]);
}
#filterDataDefault() {
return Promise.resolve([]);
}
#exploreEnvironment(rows, cache) {
this.#options.content_tag = this.content_elem.tagName.toLowerCase();
if (!rows.length) {
return;
}
if (this.#ie && this.#ie <= 9 && !this.#options.tag) {
this.#options.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
}
if (this.content_elem.children.length <= 1) {
cache.data = this.#html(rows[0] + rows[0] + rows[0]);
}
if (!this.#options.tag) {
this.#options.tag = this.content_elem.children[0].tagName.toLowerCase();
}
this.#getRowsHeight();
}
#getRowsHeight() {
const prev_item_height = this.#options.item_height;
const prev_rows_in_block = this.#options.rows_in_block;
this.#options.cluster_height = 0;
if (!this.#n_rows) {
return;
}
const nodes = this.content_elem.children;
if (!nodes.length) {
return;
}
const node = nodes[Math.floor(nodes.length / 2)];
this.#options.item_height = node.offsetHeight;
// consider table's browser spacing
if (this.#options.tag === "tr" && getStyle("borderCollapse", this.content_elem) !== "collapse") {
this.#options.item_height += parseInt(getStyle("borderSpacing", this.content_elem), 10) || 0;
}
// consider margins and margins collapsing
if (this.#options.tag !== "tr") {
const margin_top = parseInt(getStyle("marginTop", node), 10) || 0;
const margin_bottom = parseInt(getStyle("marginBottom", node), 10) || 0;
this.#options.item_height += Math.max(margin_top, margin_bottom);
}
// Update rows in block to match the number of elements that can fit in the scroll element view.
this.#options.rows_in_block = parseInt(this.scroll_elem.clientHeight / this.#options.item_height);
this.#options.block_height = this.#options.item_height * this.#options.rows_in_block;
this.#options.rows_in_cluster = this.#options.blocks_in_cluster * this.#options.rows_in_block;
this.#options.cluster_height = this.#options.blocks_in_cluster * this.#options.block_height;
return prev_item_height !== this.#options.item_height || prev_rows_in_block !== this.#options.rows_in_block;
}
#getClusterNum() {
this.#options.scroll_top = this.scroll_elem.scrollTop;
const cluster_divider = this.#options.cluster_height - this.#options.block_height;
const current_cluster = Math.floor(this.#options.scroll_top / cluster_divider);
const max_cluster = Math.floor((this.#n_rows * this.#options.item_height) / cluster_divider);
return Math.min(current_cluster, max_cluster);
}
#generateEmptyRow() {
if (!this.#options.tag || !this.#options.show_no_data_row) {
return [];
}
const empty_row = document.createElement(this.#options.tag);
const no_data_content = document.createTextNode(this.#options.no_data_text);
empty_row.className = this.#options.no_data_class;
if (this.#options.tag === "tr") {
const td = document.createElement("td");
// fixes #53
td.colSpan = 100;
td.appendChild(no_data_content);
empty_row.appendChild(td);
} else {
empty_row.appendChild(no_data_content);
}
return [empty_row.outerHTML];
}
async #generate() {
const items_start = Math.max((this.#options.rows_in_cluster - this.#options.rows_in_block) * this.#getClusterNum(), 0);
const items_end = items_start + this.#options.rows_in_cluster;
const top_offset = Math.max(items_start * this.#options.item_height, 0);
const bottom_offset = Math.max((this.#n_rows - items_end) * this.#options.item_height, 0);
const rows_above = top_offset < 1 ? items_start + 1 : items_start;
const this_cluster_rows = await this.#options.callbacks.fetchData(items_start, items_end);
return {
top_offset: top_offset,
bottom_offset: bottom_offset,
rows_above: rows_above,
rows: this_cluster_rows,
};
}
async #insertToDOM() {
if (!this.#options.cluster_height) {
const rows = await this.#options.callbacks.fetchData(0, 1);
this.#exploreEnvironment(rows, this.#cache);
}
const data = await this.#generate();
const this_cluster_rows = data.rows.join("");
const this_cluster_content_changed = this.#checkChanges("data", this_cluster_rows, this.#cache);
const top_offset_changed = this.#checkChanges("top", data.top_offset, this.#cache);
const only_bottom_offset_changed = this.#checkChanges("bottom", data.bottom_offset, this.#cache);
const layout = [];
if (this_cluster_content_changed || top_offset_changed) {
if (data.top_offset) {
this.#options.keep_parity && layout.push(this.#renderExtraTag("keep-parity"));
layout.push(this.#renderExtraTag("top-space", data.top_offset));
}
layout.push(this_cluster_rows);
data.bottom_offset && layout.push(this.#renderExtraTag("bottom-space", data.bottom_offset));
this.#options.callbacks.clusterWillChange && this.#options.callbacks.clusterWillChange();
this.#html(layout.join(""));
this.#options.content_tag === "ol" && this.content_elem.setAttribute("start", data.rows_above);
this.content_elem.style["counter-increment"] = `clusterize-counter ${data.rows_above - 1}`;
this.#options.callbacks.clusterChanged && this.#options.callbacks.clusterChanged();
} else if (only_bottom_offset_changed) {
this.content_elem.lastChild.style.height = `${data.bottom_offset}px`;
}
}
#html(data) {
const content_elem = this.content_elem;
if (this.#ie && this.#ie <= 9 && this.#options.tag === "tr") {
const div = document.createElement("div");
let last;
div.innerHTML = `<table><tbody>${data}</tbody></table>`;
while ((last = content_elem.lastChild)) {
content_elem.removeChild(last);
}
const rows_nodes = this.#getChildNodes(div.firstChild.firstChild);
while (rows_nodes.length) {
content_elem.appendChild(rows_nodes.shift());
}
} else {
content_elem.innerHTML = data;
}
}
#renderExtraTag(class_name, height) {
const tag = document.createElement(this.#options.tag);
const clusterize_prefix = "clusterize-";
tag.className = [
`${clusterize_prefix}extra-row`,
`${clusterize_prefix}${class_name}`,
].join(" ");
height && (tag.style.height = `${height}px`);
return tag.outerHTML;
}
#getChildNodes(tag) {
const child_nodes = tag.children;
const nodes = [];
for (let i = 0, j = child_nodes.length; i < j; i++) {
nodes.push(child_nodes[i]);
}
return nodes;
}
#checkChanges(type, value, cache) {
const changed = value !== cache[type];
cache[type] = value;
return changed;
}
// ==== EVENT HANDLERS ====
async #onScroll() {
if (this.#is_mac) {
if (!this.#pointer_events_set) {
this.content_elem.style.pointerEvents = "none";
this.#pointer_events_set = true;
clearTimeout(this.#scroll_debounce);
this.#scroll_debounce = setTimeout(() => {
this.content_elem.style.pointerEvents = "auto";
this.#pointer_events_set = false;
}, SCROLL_DEBOUNCE_TIME_MS);
}
}
if (this.#last_cluster !== (this.#last_cluster = this.#getClusterNum())) {
await this.#insertToDOM();
}
if (this.#options.callbacks.scrollingProgress) {
this.#options.callbacks.scrollingProgress(this.getScrollingProgress());
}
}
async #onResize() {
await this.refresh();
}
#fixElementReferences() {
if (!isElement(this.scroll_elem) || !isElement(this.content_elem)) {
return;
}
// If association for elements is broken, replace them with instance version.
if (!this.scroll_elem.isConnected || !this.content_elem.isConnected) {
document.getElementByid(this.scroll_id).replaceWith(this.scroll_elem);
// refresh since sizes may have changed.
this.refresh(true);
}
}
#setupElementObservers() {
/** Listens for changes to the scroll and content elements.
*
* During testing, the scroll/content elements would frequently get removed from
* the DOM. This instance stores a reference to these elements
* which breaks whenever these elements are removed from the DOM. To fix this,
* we need to check for these changes and re-attach our stores elements by
* replacing the ones in the DOM with the ones in our clusterize instance.
*/
this.#element_observer = new MutationObserver((mutations) => {
const scroll_elem = document.getElementById(this.scroll_id);
if (isElement(scroll_elem) && scroll_elem !== this.scroll_elem) {
clearTimeout(this.#element_observer_timer);
this.#element_observer_timer = setTimeout(
this.#fixElementReferences,
ELEMENT_OBSERVER_DEBOUNCE_TIME_MS,
);
}
const content_elem = document.getElementById(this.content_id);
if (isElement(content_elem) && content_elem !== this.content_elem) {
clearTimeout(this.#element_observer_timer);
this.#element_observer_timer = setTimeout(
this.#fixElementReferences,
ELEMENT_OBSERVER_DEBOUNCE_TIME_MS,
);
}
});
const options = { subtree: true, childList: true, attributes: true };
this.#element_observer.observe(document, options);
}
#teardownElementObservers() {
if (!isNullOrUndefined(this.#element_observer)) {
this.#element_observer.takeRecords();
this.#element_observer.disconnect();
}
this.#element_observer = null;
}
#setupResizeObservers() {
/** Handles any updates to the size of both the Scroll and Content elements. */
this.#resize_observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target.id === this.scroll_id || entry.target.id === this.content_id) {
// debounce the event
clearTimeout(this.#resize_observer_timer);
this.#resize_observer_timer = setTimeout(
() => this.#onResize(),
RESIZE_OBSERVER_DEBOUNCE_TIME_MS,
);
}
}
});
this.#resize_observer.observe(this.scroll_elem);
this.#resize_observer.observe(this.content_elem);
}
#teardownResizeObservers() {
if (!isNullOrUndefined(this.#resize_observer)) {
this.#resize_observer.disconnect();
}
if (!isNullOrUndefined(this.#resize_observer_timer)) {
clearTimeout(this.#resize_observer_timer);
}
this.#resize_observer = null;
this.#resize_observer_timer = null;
}
// ==== HELPER FUNCTIONS ====
#setupEvent(type, elem, listener) {
if (elem.addEventListener) {
return elem.addEventListener(type, event => listener.call(this), false);
} else {
return elem.attachEvent(`on${type}`, event => listener.call(this));
}
}
#teardownEvent(type, elem, listener) {
if (elem.removeEventListener) {
return elem.removeEventListener(type, event => listener.call(this), false);
} else {
return elem.detachEvent(`on${type}`, event => listener.call(this));
}
}
}

View file

@ -1,18 +0,0 @@
/* eslint-disable */
/* Clusterize.js - v0.19.0 - 2021-12-19
http://NeXTs.github.com/Clusterize.js/
Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
;(function(p,m){"undefined"!=typeof module?module.exports=m():"function"==typeof define&&"object"==typeof define.amd?define(m):this[p]=m()})("Clusterize",function(){function p(b,a,c){return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)}function m(b,a,c){return a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent("on"+b,c)}function t(b){return"[object Array]"===Object.prototype.toString.call(b)}function q(b,a){return window.getComputedStyle?window.getComputedStyle(a)[b]:
a.currentStyle[b]}var r=function(){for(var b=3,a=document.createElement("b"),c=a.all||[];a.innerHTML="\x3c!--[if gt IE "+ ++b+"]><i><![endif]--\x3e",c[0];);return 4<b?b:document.documentMode}(),C=navigator.platform.toLowerCase().indexOf("mac")+1,n=function(b){if(!(this instanceof n))return new n(b);var a=this,c={rows_in_block:50,blocks_in_cluster:4,tag:null,show_no_data_row:!0,no_data_class:"clusterize-no-data",no_data_text:"No data",keep_parity:!0,callbacks:{}};a.options={};for(var d="rows_in_block blocks_in_cluster show_no_data_row no_data_class no_data_text keep_parity tag callbacks".split(" "),
h=0,g;g=d[h];h++)a.options[g]="undefined"!=typeof b[g]&&null!=b[g]?b[g]:c[g];c=["scroll","content"];for(h=0;d=c[h];h++)if(a[d+"_elem"]=b[d+"Id"]?document.getElementById(b[d+"Id"]):b[d+"Elem"],!a[d+"_elem"])throw Error("Error! Could not find "+d+" element");a.content_elem.hasAttribute("tabindex")||a.content_elem.setAttribute("tabindex",0);var e=t(b.rows)?b.rows:a.fetchMarkup(),k={};b=a.scroll_elem.scrollTop;a.insertToDOM(e,k);a.scroll_elem.scrollTop=b;var l=!1,w=0,u=!1,x=function(){C&&(u||(a.content_elem.style.pointerEvents=
"none"),u=!0,clearTimeout(w),w=setTimeout(function(){a.content_elem.style.pointerEvents="auto";u=!1},50));l!=(l=a.getClusterNum(e))&&a.insertToDOM(e,k);a.options.callbacks.scrollingProgress&&a.options.callbacks.scrollingProgress(a.getScrollProgress())},y=0,z=function(){clearTimeout(y);y=setTimeout(a.refresh,100)};p("scroll",a.scroll_elem,x);p("resize",window,z);a.destroy=function(f){m("scroll",a.scroll_elem,x);m("resize",window,z);a.html((f?a.generateEmptyRow():e).join(""))};a.refresh=function(f){(a.getRowsHeight(e)||
f)&&a.update(e)};a.update=function(f){e=t(f)?f:[];f=a.scroll_elem.scrollTop;e.length*a.options.item_height<f&&(l=a.scroll_elem.scrollTop=0);a.insertToDOM(e,k);a.scroll_elem.scrollTop=f};a.clear=function(){a.update([])};a.getRowsAmount=function(){return e.length};a.getScrollProgress=function(){return this.options.scroll_top/(e.length*this.options.item_height)*100||0};var B=function(f,A){var v=t(A)?A:[];v.length&&(e="append"==f?e.concat(v):v.concat(e),a.insertToDOM(e,k))};a.append=function(f){B("append",
f)};a.prepend=function(f){B("prepend",f)}};n.prototype={constructor:n,fetchMarkup:function(){for(var b=[],a=this.getChildNodes(this.content_elem);a.length;)b.push(a.shift().outerHTML);return b},exploreEnvironment:function(b,a){var c=this.options;c.content_tag=this.content_elem.tagName.toLowerCase();b.length&&(r&&9>=r&&!c.tag&&(c.tag=b[0].match(/<([^>\s/]*)/)[1].toLowerCase()),1>=this.content_elem.children.length&&(a.data=this.html(b[0]+b[0]+b[0])),c.tag||(c.tag=this.content_elem.children[0].tagName.toLowerCase()),
this.getRowsHeight(b))},getRowsHeight:function(b){var a=this.options,c=a.item_height;a.cluster_height=0;if(b.length&&(b=this.content_elem.children,b.length)){var d=b[Math.floor(b.length/2)];a.item_height=d.offsetHeight;"tr"==a.tag&&"collapse"!=q("borderCollapse",this.content_elem)&&(a.item_height+=parseInt(q("borderSpacing",this.content_elem),10)||0);"tr"!=a.tag&&(b=parseInt(q("marginTop",d),10)||0,d=parseInt(q("marginBottom",d),10)||0,a.item_height+=Math.max(b,d));a.block_height=a.item_height*a.rows_in_block;
a.rows_in_cluster=a.blocks_in_cluster*a.rows_in_block;a.cluster_height=a.blocks_in_cluster*a.block_height;return c!=a.item_height}},getClusterNum:function(b){var a=this.options;a.scroll_top=this.scroll_elem.scrollTop;var c=a.cluster_height-a.block_height;return Math.min(Math.floor(a.scroll_top/c),Math.floor(b.length*a.item_height/c))},generateEmptyRow:function(){var b=this.options;if(!b.tag||!b.show_no_data_row)return[];var a=document.createElement(b.tag),c=document.createTextNode(b.no_data_text);
a.className=b.no_data_class;if("tr"==b.tag){var d=document.createElement("td");d.colSpan=100;d.appendChild(c)}a.appendChild(d||c);return[a.outerHTML]},generate:function(b){var a=this.options,c=b.length;if(c<a.rows_in_block)return{top_offset:0,bottom_offset:0,rows_above:0,rows:c?b:this.generateEmptyRow()};var d=Math.max((a.rows_in_cluster-a.rows_in_block)*this.getClusterNum(b),0),h=d+a.rows_in_cluster,g=Math.max(d*a.item_height,0);a=Math.max((c-h)*a.item_height,0);c=[];var e=d;for(1>g&&e++;d<h;d++)b[d]&&
c.push(b[d]);return{top_offset:g,bottom_offset:a,rows_above:e,rows:c}},renderExtraTag:function(b,a){var c=document.createElement(this.options.tag);c.className=["clusterize-extra-row","clusterize-"+b].join(" ");a&&(c.style.height=a+"px");return c.outerHTML},insertToDOM:function(b,a){this.options.cluster_height||this.exploreEnvironment(b,a);var c=this.generate(b),d=c.rows.join(""),h=this.checkChanges("data",d,a),g=this.checkChanges("top",c.top_offset,a),e=this.checkChanges("bottom",c.bottom_offset,
a),k=this.options.callbacks,l=[];h||g?(c.top_offset&&(this.options.keep_parity&&l.push(this.renderExtraTag("keep-parity")),l.push(this.renderExtraTag("top-space",c.top_offset))),l.push(d),c.bottom_offset&&l.push(this.renderExtraTag("bottom-space",c.bottom_offset)),k.clusterWillChange&&k.clusterWillChange(),this.html(l.join("")),"ol"==this.options.content_tag&&this.content_elem.setAttribute("start",c.rows_above),this.content_elem.style["counter-increment"]="clusterize-counter "+(c.rows_above-1),k.clusterChanged&&
k.clusterChanged()):e&&(this.content_elem.lastChild.style.height=c.bottom_offset+"px")},html:function(b){var a=this.content_elem;if(r&&9>=r&&"tr"==this.options.tag){var c=document.createElement("div");for(c.innerHTML="<table><tbody>"+b+"</tbody></table>";b=a.lastChild;)a.removeChild(b);for(c=this.getChildNodes(c.firstChild.firstChild);c.length;)a.appendChild(c.shift())}else a.innerHTML=b},getChildNodes:function(b){b=b.children;for(var a=[],c=0,d=b.length;c<d;c++)a.push(b[c]);return a},checkChanges:function(b,
a,c){var d=a!=c[b];c[b]=a;return d}};return n});

View file

@ -20,170 +20,6 @@ const extraPageUserMetadataEditors = {};
// A flag used by the `waitForBool` promise to determine when we first load Ui Options.
const initialUiOptionsLoaded = {state: false};
/** Helper functions for checking types and simplifying logging. */
const isString = x => typeof x === "string" || x instanceof String;
const isStringLogError = x => {
if (isString(x)) {
return true;
}
console.error("expected string, got:", typeof x);
return false;
};
const isNull = x => x === null;
const isUndefined = x => typeof x === "undefined" || x === undefined;
// checks both null and undefined for simplicity sake.
const isNullOrUndefined = x => isNull(x) || isUndefined(x);
const isNullOrUndefinedLogError = x => {
if (isNullOrUndefined(x)) {
console.error("Variable is null/undefined.");
return true;
}
return false;
};
const isElement = x => x instanceof Element;
const isElementLogError = x => {
if (isElement(x)) {
return true;
}
console.error("expected element type, got:", typeof x);
return false;
};
const isFunction = x => typeof x === "function";
const isFunctionLogError = x => {
if (isFunction(x)) {
return true;
}
console.error("expected function type, got:", typeof x);
return false;
};
const getElementByIdLogError = selector => {
let elem = gradioApp().getElementById(selector);
isElementLogError(elem);
return elem;
};
const querySelectorLogError = selector => {
let elem = gradioApp().querySelector(selector);
isElementLogError(elem);
return elem;
};
const debounce = (handler, timeout_ms) => {
/** Debounces a function call.
*
* NOTE: This will NOT work if called from within a class.
* It will drop `this` from scope.
*
* Repeated calls to the debounce handler will not call the handler until there are
* no new calls to the debounce handler for timeout_ms time.
*
* Example:
* function add(x, y) { return x + y; }
* let debounce_handler = debounce(add, 5000);
* let res;
* for (let i = 0; i < 10; i++) {
* res = debounce_handler(i, 100);
* }
* console.log("Result:", res);
*
* This example will print "Result: 109".
*/
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => handler(...args), timeout_ms);
};
};
const waitForElement = selector => {
/** Promise that waits for an element to exist in DOM. */
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
});
};
const waitForBool = o => {
/** Promise that waits for a boolean to be true.
*
* `o` must be an Object of the form:
* { state: <bool value> }
*
* Resolves when (state === true)
*/
return new Promise(resolve => {
(function _waitForBool() {
if (o.state) {
return resolve();
}
setTimeout(_waitForBool, 100);
})();
});
};
const waitForKeyInObject = o => {
/** Promise that waits for a key to exist in an object.
*
* `o` must be an Object of the form:
* {
* obj: <object to watch for key>,
* k: <key to watch for>,
* }
*
* Resolves when (k in obj)
*/
return new Promise(resolve => {
(function _waitForKeyInObject() {
if (o.k in o.obj) {
return resolve();
}
setTimeout(_waitForKeyInObject, 100);
})();
});
};
const waitForValueInObject = o => {
/** Promise that waits for a key value pair in an Object.
*
* `o` must be an Object of the form:
* {
* obj: <object containing value>,
* k: <key in object>,
* v: <value at key for comparison>
* }
*
* Resolves when obj[k] == v
*/
return new Promise(resolve => {
waitForKeyInObject({k: o.k, obj: o.obj}).then(() => {
(function _waitForValueInObject() {
if (o.k in o.obj && o.obj[o.k] == o.v) {
return resolve();
}
setTimeout(_waitForValueInObject, 100);
})();
});
});
};
function toggleCss(key, css, enable) {
var style = document.getElementById(key);
if (enable && !style) {

View file

@ -29,60 +29,6 @@ class InvalidCompressedJsonDataError extends Error {
}
}
const getComputedPropertyDims = (elem, prop) => {
/** Returns the top/left/bottom/right float dimensions of an element for the specified property. */
const style = window.getComputedStyle(elem, null);
return {
top: parseFloat(style.getPropertyValue(`${prop}-top`)),
left: parseFloat(style.getPropertyValue(`${prop}-left`)),
bottom: parseFloat(style.getPropertyValue(`${prop}-bottom`)),
right: parseFloat(style.getPropertyValue(`${prop}-right`)),
};
};
const getComputedMarginDims = elem => {
/** Returns the width/height of the computed margin of an element. */
const dims = getComputedPropertyDims(elem, "margin");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
};
const getComputedPaddingDims = elem => {
/** Returns the width/height of the computed padding of an element. */
const dims = getComputedPropertyDims(elem, "padding");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
};
const getComputedBorderDims = elem => {
/** Returns the width/height of the computed border of an element. */
// computed border will always start with the pixel width so thankfully
// the parseFloat() conversion will just give us the width and ignore the rest.
// Otherwise we'd have to use border-<pos>-width instead.
const dims = getComputedPropertyDims(elem, "border");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
};
const getComputedDims = elem => {
/** Returns the full width and height of an element including its margin, padding, and border. */
const width = elem.scrollWidth;
const height = elem.scrollHeight;
const margin = getComputedMarginDims(elem);
const padding = getComputedPaddingDims(elem);
const border = getComputedBorderDims(elem);
return {
width: width + margin.width + padding.width + border.width,
height: height + margin.height + padding.height + border.height,
};
};
async function decompress(base64string) {
/** Decompresses a base64 encoded ZLIB compressed string. */
try {
@ -101,26 +47,6 @@ async function decompress(base64string) {
}
}
const htmlStringToElement = function(str) {
/** Converts an HTML string into an Element type. */
let parser = new DOMParser();
let tmp = parser.parseFromString(str, "text/html");
return tmp.body.firstElementChild;
};
const calcColsPerRow = function(parent, child) {
/** Calculates the number of columns of children that can fit in a parent's visible width. */
const parent_inner_width = parent.offsetWidth - getComputedPaddingDims(parent).width;
return parseInt(parent_inner_width / getComputedDims(child).width);
};
const calcRowsPerCol = function(parent, child) {
/** Calculates the number of rows of children that can fit in a parent's visible height. */
const parent_inner_height = parent.offsetHeight - getComputedPaddingDims(parent).height;
return parseInt(parent_inner_height / getComputedDims(child).height);
};
class ExtraNetworksClusterize {
/** Base class for a clusterize list. Cannot be used directly. */
constructor(

299
javascript/utils.js Normal file
View file

@ -0,0 +1,299 @@
/** Helper functions for checking types and simplifying logging/error handling. */
const isNumber = x => typeof x === "number" && isFinite(x);
const isNumberLogError = x => {
if (isNumber(x)) {
return true;
}
console.error("expected number, got:", typeof x);
return false;
}
const isNumberThrowError = x => {
if (isNumber(x)) {
return;
}
throw new Error("expected number, got:", typeof x);
}
const isString = x => typeof x === "string" || x instanceof String;
const isStringLogError = x => {
if (isString(x)) {
return true;
}
console.error("expected string, got:", typeof x);
return false;
};
const isStringThrowError = x => {
if (isString(x)) {
return;
}
throw new Error("expected string, got:", typeof x);
};
const isNull = x => x === null;
const isUndefined = x => typeof x === "undefined" || x === undefined;
// checks both null and undefined for simplicity sake.
const isNullOrUndefined = x => isNull(x) || isUndefined(x);
const isNullOrUndefinedLogError = x => {
if (isNullOrUndefined(x)) {
console.error("Variable is null/undefined.");
return true;
}
return false;
};
const isNullOrUndefinedThrowError = x => {
if (!isNullOrUndefined(x)) {
return;
}
throw new Error("Variable is null/undefined.");
};
const isElement = x => x instanceof Element;
const isElementLogError = x => {
if (isElement(x)) {
return true;
}
console.error("expected element type, got:", typeof x);
return false;
};
const isElementThrowError = x => {
if (isElement(x)) {
return;
}
throw new Error("expected element type, got:", typeof x);
};
const isFunction = x => typeof x === "function";
const isFunctionLogError = x => {
if (isFunction(x)) {
return true;
}
console.error("expected function type, got:", typeof x);
return false;
};
const isFunctionThrowError = x => {
if (isFunction(x)) {
return;
}
throw new Error("expected function type, got:", typeof x);
};
const getElementByIdLogError = selector => {
const elem = gradioApp().getElementById(selector);
isElementLogError(elem);
return elem;
};
const getElementByIdThrowError = selector => {
const elem = gradioApp().getElementById(selector);
isElementThrowError(elem);
return elem;
};
const querySelectorLogError = selector => {
const elem = gradioApp().querySelector(selector);
isElementLogError(elem);
return elem;
};
const querySelectorThrowError = selector => {
const elem = gradioApp().querySelector(selector);
isElementThrowError(elem);
return elem;
};
/** Functions for getting dimensions of elements. */
const getComputedPropertyDims = (elem, prop) => {
/** Returns the top/left/bottom/right float dimensions of an element for the specified property. */
const style = window.getComputedStyle(elem, null);
return {
top: parseFloat(style.getPropertyValue(`${prop}-top`)),
left: parseFloat(style.getPropertyValue(`${prop}-left`)),
bottom: parseFloat(style.getPropertyValue(`${prop}-bottom`)),
right: parseFloat(style.getPropertyValue(`${prop}-right`)),
};
};
const getComputedMarginDims = elem => {
/** Returns the width/height of the computed margin of an element. */
const dims = getComputedPropertyDims(elem, "margin");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
};
const getComputedPaddingDims = elem => {
/** Returns the width/height of the computed padding of an element. */
const dims = getComputedPropertyDims(elem, "padding");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
};
const getComputedBorderDims = elem => {
/** Returns the width/height of the computed border of an element. */
// computed border will always start with the pixel width so thankfully
// the parseFloat() conversion will just give us the width and ignore the rest.
// Otherwise we'd have to use border-<pos>-width instead.
const dims = getComputedPropertyDims(elem, "border");
return {
width: dims.left + dims.right,
height: dims.top + dims.bottom,
};
};
const getComputedDims = elem => {
/** Returns the full width and height of an element including its margin, padding, and border. */
const width = elem.scrollWidth;
const height = elem.scrollHeight;
const margin = getComputedMarginDims(elem);
const padding = getComputedPaddingDims(elem);
const border = getComputedBorderDims(elem);
return {
width: width + margin.width + padding.width + border.width,
height: height + margin.height + padding.height + border.height,
};
};
const calcColsPerRow = function(parent, child) {
/** Calculates the number of columns of children that can fit in a parent's visible width. */
const parent_inner_width = parent.offsetWidth - getComputedPaddingDims(parent).width;
return parseInt(parent_inner_width / getComputedDims(child).width);
};
const calcRowsPerCol = function(parent, child) {
/** Calculates the number of rows of children that can fit in a parent's visible height. */
const parent_inner_height = parent.offsetHeight - getComputedPaddingDims(parent).height;
return parseInt(parent_inner_height / getComputedDims(child).height);
};
/** Functions for asynchronous operations. */
const debounce = (handler, timeout_ms) => {
/** Debounces a function call.
*
* NOTE: This will NOT work if called from within a class.
* It will drop `this` from scope.
*
* Repeated calls to the debounce handler will not call the handler until there are
* no new calls to the debounce handler for timeout_ms time.
*
* Example:
* function add(x, y) { return x + y; }
* let debounce_handler = debounce(add, 5000);
* let res;
* for (let i = 0; i < 10; i++) {
* res = debounce_handler(i, 100);
* }
* console.log("Result:", res);
*
* This example will print "Result: 109".
*/
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => handler(...args), timeout_ms);
};
};
const waitForElement = selector => {
/** Promise that waits for an element to exist in DOM. */
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
});
};
const waitForBool = o => {
/** Promise that waits for a boolean to be true.
*
* `o` must be an Object of the form:
* { state: <bool value> }
*
* Resolves when (state === true)
*/
return new Promise(resolve => {
(function _waitForBool() {
if (o.state) {
return resolve();
}
setTimeout(_waitForBool, 100);
})();
});
};
const waitForKeyInObject = o => {
/** Promise that waits for a key to exist in an object.
*
* `o` must be an Object of the form:
* {
* obj: <object to watch for key>,
* k: <key to watch for>,
* }
*
* Resolves when (k in obj)
*/
return new Promise(resolve => {
(function _waitForKeyInObject() {
if (o.k in o.obj) {
return resolve();
}
setTimeout(_waitForKeyInObject, 100);
})();
});
};
const waitForValueInObject = o => {
/** Promise that waits for a key value pair in an Object.
*
* `o` must be an Object of the form:
* {
* obj: <object containing value>,
* k: <key in object>,
* v: <value at key for comparison>
* }
*
* Resolves when obj[k] == v
*/
return new Promise(resolve => {
waitForKeyInObject({k: o.k, obj: o.obj}).then(() => {
(function _waitForValueInObject() {
if (o.k in o.obj && o.obj[o.k] == o.v) {
return resolve();
}
setTimeout(_waitForValueInObject, 100);
})();
});
});
};
/** Misc helper functions. */
const clamp = (x, min, max) => Math.max(min, Math.min(x, max));
const getStyle = (prop, elem) => {
return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
}
const htmlStringToElement = function(str) {
/** Converts an HTML string into an Element type. */
let parser = new DOMParser();
let tmp = parser.parseFromString(str, "text/html");
return tmp.body.firstElementChild;
};