mirror of
https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
synced 2026-01-31 04:42:09 -08:00
Merge cd16f56b1f into fd68e0c384
This commit is contained in:
commit
68579b9f78
34 changed files with 8027 additions and 1629 deletions
|
|
@ -1,11 +1,16 @@
|
|||
import os
|
||||
import html
|
||||
import datetime
|
||||
import math
|
||||
import matplotlib as mpl
|
||||
import colorsys
|
||||
|
||||
import network
|
||||
import networks
|
||||
|
||||
from modules import shared, ui_extra_networks
|
||||
from modules.ui_extra_networks import quote_js
|
||||
from ui_edit_user_metadata import LoraUserMetadataEditor
|
||||
from ui_edit_user_metadata import LoraUserMetadataEditor, build_tags
|
||||
|
||||
|
||||
class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
|
||||
|
|
@ -14,6 +19,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
|
|||
|
||||
def refresh(self):
|
||||
networks.list_available_networks()
|
||||
super().refresh()
|
||||
|
||||
def create_item(self, name, index=None, enable_filter=True):
|
||||
lora_on_disk = networks.available_networks.get(name)
|
||||
|
|
@ -43,10 +49,10 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
|
|||
self.read_user_metadata(item)
|
||||
activation_text = item["user_metadata"].get("activation text")
|
||||
preferred_weight = item["user_metadata"].get("preferred weight", 0.0)
|
||||
item["prompt"] = quote_js(f"<lora:{alias}:") + " + " + (str(preferred_weight) if preferred_weight else "opts.extra_networks_default_multiplier") + " + " + quote_js(">")
|
||||
|
||||
prompt = f"<lora:{alias}:{str(preferred_weight) if preferred_weight else shared.opts.extra_networks_default_multiplier}>"
|
||||
if activation_text:
|
||||
item["prompt"] += " + " + quote_js(" " + activation_text)
|
||||
prompt += f" {activation_text}"
|
||||
item["prompt"] = quote_js(prompt)
|
||||
|
||||
negative_prompt = item["user_metadata"].get("negative text")
|
||||
item["negative_prompt"] = quote_js("")
|
||||
|
|
@ -88,3 +94,148 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage):
|
|||
|
||||
def create_user_metadata_editor(self, ui, tabname):
|
||||
return LoraUserMetadataEditor(ui, tabname, self)
|
||||
|
||||
def get_model_detail_metadata_table(self, model_name: str) -> str:
|
||||
res = super().get_model_detail_metadata_table(model_name)
|
||||
|
||||
metadata = self.metadata.get(model_name)
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
|
||||
keys = {
|
||||
'ss_output_name': "Output name:",
|
||||
'ss_sd_model_name': "Model:",
|
||||
'ss_clip_skip': "Clip skip:",
|
||||
'ss_network_module': "Kohya module:",
|
||||
}
|
||||
|
||||
params = []
|
||||
|
||||
for k, lbl in keys.items():
|
||||
v = metadata.get(k, None)
|
||||
if v is not None and str(v) != "None":
|
||||
params.append((lbl, html.escape(v)))
|
||||
|
||||
ss_training_started_at = metadata.get('ss_training_started_at')
|
||||
if ss_training_started_at:
|
||||
date_trained = datetime.datetime.utcfromtimestamp(
|
||||
float(ss_training_started_at)
|
||||
).strftime('%Y-%m-%d %H:%M')
|
||||
params.append(("Date trained:", date_trained))
|
||||
|
||||
ss_bucket_info = metadata.get("ss_bucket_info")
|
||||
if ss_bucket_info and "buckets" in ss_bucket_info:
|
||||
resolutions = {}
|
||||
for _, bucket in ss_bucket_info["buckets"].items():
|
||||
resolution = bucket["resolution"]
|
||||
resolution = f'{resolution[1]}x{resolution[0]}'
|
||||
resolutions[resolution] = resolutions.get(resolution, 0) + int(bucket["count"])
|
||||
|
||||
resolutions_list = sorted(resolutions.keys(), key=resolutions.get, reverse=True)
|
||||
resolutions_text = html.escape(", ".join(resolutions_list))
|
||||
resolutions_text = (
|
||||
"<div class='styled-scrollbar' style='overflow-x: auto'>"
|
||||
f"{resolutions_text}"
|
||||
"</div>"
|
||||
)
|
||||
params.append(("Resolutions:", resolutions_text))
|
||||
|
||||
image_count = 0
|
||||
for v in metadata.get("ss_dataset_dirs", {}).values():
|
||||
image_count += int(v.get("img_count", 0))
|
||||
|
||||
if image_count:
|
||||
params.append(("Dataset size:", image_count))
|
||||
|
||||
tbl_metadata = "".join([f"<tr><th>{tr[0]}</th><td>{tr[1]}</td>" for tr in params])
|
||||
|
||||
return res + tbl_metadata
|
||||
|
||||
def get_model_detail_extra_html(self, model_name: str) -> str:
|
||||
"""Generates HTML to show in the details view."""
|
||||
res = ""
|
||||
|
||||
item = self.items.get(model_name, {})
|
||||
metadata = item.get("metadata", {}) or {}
|
||||
user_metadata = item.get("user_metadata", {}) or {}
|
||||
|
||||
sd_version = item.get("sd_version", None)
|
||||
preferred_weight = user_metadata.get("preferred weight", None)
|
||||
activation_text = user_metadata.get("activation text", None)
|
||||
negative_text = user_metadata.get("negative text", None)
|
||||
|
||||
rows = []
|
||||
|
||||
if sd_version is not None:
|
||||
rows.append(("SD Version:", sd_version))
|
||||
|
||||
if preferred_weight is not None:
|
||||
rows.append(("Preferred weight:", preferred_weight))
|
||||
|
||||
if activation_text is not None:
|
||||
rows.append(("Activation text:", activation_text))
|
||||
|
||||
if negative_text is not None:
|
||||
rows.append(("Negative propmt:", negative_text))
|
||||
|
||||
rows_html = "".join([f"<tr><th>{tr[0]}</th><td>{tr[1]}</td>" for tr in rows])
|
||||
|
||||
if rows_html:
|
||||
res += "<h3>User Metadata</h3>"
|
||||
res += f"<table><tbody>{rows_html}</tbody></table>"
|
||||
|
||||
tags = build_tags(metadata)
|
||||
if tags is None or len(tags) == 0:
|
||||
res += "<h3>Model Tags</h3>"
|
||||
res += "<div class='model-info--tags'>Metadata contains no tags</div>"
|
||||
return res
|
||||
|
||||
min_tag = min(int(x[1]) for x in tags)
|
||||
max_tag = max(int(x[1]) for x in tags)
|
||||
|
||||
cmap = mpl.colormaps["coolwarm"]
|
||||
|
||||
def _clamp(x: float, min_val: float, max_val: float) -> float:
|
||||
return max(min_val, min(x, max_val))
|
||||
|
||||
def _get_fg_color(r, g, b) -> str:
|
||||
return "#000000" if (r * 0.299 + g * 0.587 + b * 0.114) > 0.5 else "#FFFFFF"
|
||||
|
||||
tag_elems = []
|
||||
for (tag_name, tag_count) in tags:
|
||||
# Normalize tag count
|
||||
tag_count = int(tag_count)
|
||||
if min_tag == max_tag: # Prevent DivideByZero error.
|
||||
cmap_idx = cmap.N // 2
|
||||
else:
|
||||
cmap_idx = math.floor(
|
||||
(tag_count - min_tag) / (max_tag - min_tag) * (cmap.N - 1)
|
||||
)
|
||||
|
||||
# Get the bg color based on tag count and a contrasting fg color.
|
||||
base_color = cmap(cmap_idx)
|
||||
base_color = [_clamp(x, 0, 1) for x in base_color]
|
||||
base_fg_color = _get_fg_color(*base_color[:3])
|
||||
# Now get a slightly darker background for the tag count bg color.
|
||||
h, lum, s = colorsys.rgb_to_hls(*base_color[:3])
|
||||
lum = max(min(lum * 0.7, 1.0), 0.0)
|
||||
dark_color = colorsys.hls_to_rgb(h, lum, s)
|
||||
dark_color = [_clamp(x, 0, 1) for x in dark_color]
|
||||
dark_fg_color = _get_fg_color(*dark_color[:3])
|
||||
# Convert the colors to a hex string.
|
||||
base_color = mpl.colors.rgb2hex(base_color)
|
||||
dark_color = mpl.colors.rgb2hex(dark_color)
|
||||
# Finally, generate the HTML for this tag.
|
||||
tag_style = f"background: {base_color};"
|
||||
name_style = f"color: {base_fg_color};"
|
||||
count_style = f"background: {dark_color}; color: {dark_fg_color};"
|
||||
|
||||
tag_elems.append((
|
||||
f"<span class='model-info--tag' style='{tag_style}'>"
|
||||
f"<span class='model-info--tag-name' style='{name_style}'>{tag_name}</span>"
|
||||
f"<span class='model-info--tag-count' style='{count_style}'>{tag_count}</span>"
|
||||
"</span>"
|
||||
))
|
||||
res += "<h3>Model Tags</h3>"
|
||||
res += f"<div class='model-info--tags'>{''.join(tag_elems)}</div>"
|
||||
return res
|
||||
|
|
|
|||
11
html/extra-networks-btn-chevron.html
Normal file
11
html/extra-networks-btn-chevron.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div class="tree-list-item-action--chevron {extra_classes}">
|
||||
<svg class="chevron-icon-single" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M 4 4 H 12 V 12" />
|
||||
</svg>
|
||||
<svg class="chevron-icon-double" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M 5 3 H 13 V 11" />
|
||||
<path d="M 1 7 H 9 V 15" />
|
||||
</svg>
|
||||
</div>
|
||||
2
html/extra-networks-btn-copy-path.html
Normal file
2
html/extra-networks-btn-copy-path.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<div class="copy-path-button card-button" title="Copy path to clipboard" data-clipboard-text="{clipboard_text}">
|
||||
</div>
|
||||
2
html/extra-networks-btn-dirs-view-item.html
Normal file
2
html/extra-networks-btn-dirs-view-item.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<button class='lg secondary gradio-button custom-button extra-network-dirs-view-button {extra_class}' title="{title}"
|
||||
{data_attributes}>{label}</button>
|
||||
1
html/extra-networks-btn-edit-metadata.html
Normal file
1
html/extra-networks-btn-edit-metadata.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div class="edit-button card-button" title="Edit metadata"></div>
|
||||
1
html/extra-networks-btn-show-metadata.html
Normal file
1
html/extra-networks-btn-show-metadata.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div class="metadata-button card-button" title="Show internal metadata"></div>
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
<div class="card" style="{style}" onclick="{card_clicked}" data-name="{name}" {sort_keys}>
|
||||
{background_image}
|
||||
<div class="button-row">{copy_path_button}{metadata_button}{edit_button}</div>
|
||||
<div class="actions">
|
||||
<div class="additional">{search_terms}</div>
|
||||
<span class="name">{name}</span>
|
||||
<span class="description">{description}</span>
|
||||
</div>
|
||||
<div class="card" style="{style}" {data_attributes}>
|
||||
{background_image}
|
||||
{button_row}
|
||||
<div class="actions">
|
||||
<span class="name">{name}</span>
|
||||
<span class="description" {description_data_attributes}>{description}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
<div class="copy-path-button card-button"
|
||||
title="Copy path to clipboard"
|
||||
onclick="extraNetworksCopyCardPath(event)"
|
||||
data-clipboard-text="{filename}">
|
||||
</div>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<div class="edit-button card-button"
|
||||
title="Edit metadata"
|
||||
onclick="extraNetworksEditUserMetadata(event, '{tabname}', '{extra_networks_tabname}')">
|
||||
</div>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<div class="metadata-button card-button"
|
||||
title="Show internal metadata"
|
||||
onclick="extraNetworksRequestMetadata(event, '{extra_networks_tabname}')">
|
||||
</div>
|
||||
17
html/extra-networks-model-details.html
Normal file
17
html/extra-networks-model-details.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<div class="extra-network-content--dets-view-model-info">
|
||||
<div class="model-info--header">
|
||||
<h1 class="model-info--name">{name}</h1>
|
||||
<button class="extra-network-control model-info--close" title="Close model details">
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M 2 2 L 14 14 M 2 14 L 14 2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="model-info--description" {description_data_attributes}>{description}</p>
|
||||
<h3>Model Metadata</h3>
|
||||
<table class="model-info--metadata">
|
||||
<tbody>{metadata_table}</tbody>
|
||||
</table>
|
||||
{model_specific}
|
||||
</div>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<div class='nocards'>
|
||||
<h1>Nothing here. Add some content to the following directories:</h1>
|
||||
|
||||
<ul>
|
||||
{dirs}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<div class="extra-network-pane-content-dirs">
|
||||
<div id='{tabname}_{extra_networks_tabname}_dirs' class='extra-network-dirs'>
|
||||
{dirs_html}
|
||||
</div>
|
||||
<div id='{tabname}_{extra_networks_tabname}_cards' class='extra-network-cards'>
|
||||
{items_html}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<div class="extra-network-pane-content-tree resize-handle-row">
|
||||
<div id='{tabname}_{extra_networks_tabname}_tree' class='extra-network-tree' style='flex-basis: {extra_networks_tree_view_default_width}px'>
|
||||
{tree_html}
|
||||
</div>
|
||||
<div id='{tabname}_{extra_networks_tabname}_cards' class='extra-network-cards' style='flex-grow: 1;'>
|
||||
{items_html}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,81 +1,211 @@
|
|||
<div id='{tabname}_{extra_networks_tabname}_pane' class='extra-network-pane {tree_view_div_default_display_class}'>
|
||||
<div class="extra-network-control" id="{tabname}_{extra_networks_tabname}_controls" style="display:none" >
|
||||
<div id='{tabname}_{extra_networks_tabname}_pane' class='extra-network-pane' data-tabname="{tabname}"
|
||||
data-extra-networks-tabname="{extra_networks_tabname}" data-tabname-full="{tabname}_{extra_networks_tabname}">
|
||||
<div class="extra-network-controls hidden" data-tabname-full="{tabname}_{extra_networks_tabname}">
|
||||
<div class="extra-network-control--search">
|
||||
<input
|
||||
id="{tabname}_{extra_networks_tabname}_extra_search"
|
||||
class="extra-network-control--search-text"
|
||||
type="search"
|
||||
placeholder="Search"
|
||||
>
|
||||
<svg class="extra-network-control--search-icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
|
||||
<circle cx="6" cy="6" r="5" />
|
||||
<line x1="9.75" y1="9.75" x2="13" y2="13" />
|
||||
</svg>
|
||||
<input class="extra-network-control--search-text" type="text" placeholder="Search">
|
||||
<button role="button" class="extra-network-control extra-network-control--search-clear">
|
||||
<svg class="extra-network-control--search-clear-icon" viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
|
||||
<path fill="none" stroke="currentColor" d="M 5 5 L 11 11 M 5 11 L 11 5" />
|
||||
<circle fill="none" stroke="currentColor" cx="8" cy="8" r="7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<small>Sort: </small>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_sort_path"
|
||||
class="extra-network-control--sort{sort_path_active}"
|
||||
data-sortkey="default"
|
||||
title="Sort by path"
|
||||
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
|
||||
<div class="extra-network-controls--container">
|
||||
<div class="extra-network-controls--header">Clear Filters</div>
|
||||
<div class="extra-network-controls--buttons">
|
||||
<button class="extra-network-control extra-network-control--clear-filters"
|
||||
title="Clear all directory filters">
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="1">
|
||||
<path d="M 2 2 V 4 L 6 8 V 13 L 9 14 V 12 M 12 5 L 13 4 V 2 H 2" />
|
||||
<path d="M 9 10 L 12 7 M 9 7 L 12 10" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_sort_name"
|
||||
class="extra-network-control--sort{sort_name_active}"
|
||||
data-sortkey="name"
|
||||
title="Sort by name"
|
||||
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
|
||||
<div class="extra-network-controls--container">
|
||||
<div class="extra-network-controls--header">Sort Mode</div>
|
||||
<div class="extra-network-controls--buttons">
|
||||
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by path"
|
||||
data-sort-mode="path" {btn_sort_mode_path_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="1">
|
||||
<rect x="0.5" y="4" width="15" height="10" rx="1" />
|
||||
<path d="M 0.5 5 L 0.5 3 Q 0.5 2 1.5 2 L 6 2 Q 7 2 7 3 L 7 4" />
|
||||
<circle cx="7.5" cy="8.5" r="2" />
|
||||
<line x1="9" y1="10" x2="10.5" y2="11.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by name"
|
||||
data-sort-mode="name" {btn_sort_mode_name_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke-width="1" d="M 2 12 L 6 3 L 10 12 M 3.5 9 L 8.5 9" />
|
||||
<path stroke-width="1" d="M 8 3 L 15 3 M 9.5 6 L 15 6 M 11 9 L 15 9 M 12.5 12 L 15 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by date created"
|
||||
data-sort-mode="date_created" {btn_sort_mode_date_created_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
|
||||
<rect x="1" y="2" width="14" height="12" rx="2" fill="none" />
|
||||
<line x1="1" y1="6" x2="15" y2="6" />
|
||||
<line x1="8" y1="8" x2="8" y2="12" />
|
||||
<line x1="6" y1="10" x2="10" y2="10" />
|
||||
<line x1="4" y1="1" x2="4" y2="3" />
|
||||
<line x1="8" y1="1" x2="8" y2="3" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--sort-mode" title="Sort by date modified"
|
||||
data-sort-mode="date_modified" {btn_sort_mode_date_modified_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-width="1">
|
||||
<g stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="2" width="14" height="12" rx="2" />
|
||||
<line x1="1" y1="6" x2="15" y2="6" />
|
||||
<line x1="4" y1="1" x2="4" y2="3" />
|
||||
<line x1="8" y1="1" x2="8" y2="3" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
</g>
|
||||
<g stroke-linecap="square" stroke-linejoin="square">
|
||||
<path d="M 4 9 H 10 L 12 10 L 10 11 H 4 Z M 6 9 V 10" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_sort_date_created"
|
||||
class="extra-network-control--sort{sort_date_created_active}"
|
||||
data-sortkey="date_created"
|
||||
title="Sort by date created"
|
||||
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
|
||||
<div class="extra-network-controls--container">
|
||||
<div class="extra-network-controls--header">Sort Order</div>
|
||||
<div class="extra-network-controls--buttons">
|
||||
<button class="extra-network-control extra-network-control--sort-dir" title="Sort ascending"
|
||||
data-sort-dir="ascending" {btn_sort_dir_ascending_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path fill="currentColor" d="M 8 1 L 3 7 L 13 7 Z" />
|
||||
<path fill="none" stroke="var(--input-placeholder-color)" d="M 8 15 L 3 9 L 13 9 Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--sort-dir" title="Sort descending"
|
||||
data-sort-dir="descending" {btn_sort_dir_descending_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path fill="none" stroke="var(--input-placeholder-color)" d="M 8 1 L 3 7 L 13 7 Z" />
|
||||
<path fill="currentColor" d="M 8 15 L 3 9 L 13 9 Z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_sort_date_modified"
|
||||
class="extra-network-control--sort{sort_date_modified_active}"
|
||||
data-sortkey="date_modified"
|
||||
title="Sort by date modified"
|
||||
onclick="extraNetworksControlSortOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--sort-icon"></i>
|
||||
<div class="extra-network-controls--container">
|
||||
<div class="extra-network-controls--header">View Mode</div>
|
||||
<div class="extra-network-controls--buttons">
|
||||
<button class="extra-network-control extra-network-control--dirs-view" title="Enable Directory View"
|
||||
{btn_dirs_view_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" stroke="none">
|
||||
<rect x="0" y="6" width="4" height="4" rx="1" />
|
||||
<rect x="6" y="6" width="4" height="4" rx="1" />
|
||||
<rect x="12" y="6" width="4" height="4" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--tree-view" title="Enable Tree View"
|
||||
{btn_tree_view_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" stroke="none">
|
||||
<rect x="3" y="0" width="4" height="4" rx="1" />
|
||||
<rect x="9" y="6" width="4" height="4" rx="1" />
|
||||
<rect x="9" y="12" width="4" height="4" rx="1" />
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M 5 5 V 14 H 8 M 5 8 H 8" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--card-view" title="Enable Card View"
|
||||
{btn_card_view_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor" stroke="none">
|
||||
<rect x="3" y="3" width="4" height="4" rx="1" />
|
||||
<rect x="3" y="9" width="4" height="4" rx="1" />
|
||||
<rect x="9" y="3" width="4" height="4" rx="1" />
|
||||
<rect x="9" y="9" width="4" height="4" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="extra-network-control extra-network-control--dets-view" title="Enable Card Detail View"
|
||||
{btn_dets_view_data_attributes}>
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path d="M 3 3 H 12 M 3 6 H 12 M 3 9 H 12 M 3 12 H 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small> </small>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_sort_dir"
|
||||
class="extra-network-control--sort-dir"
|
||||
data-sortdir="{data_sortdir}"
|
||||
title="Sort ascending"
|
||||
onclick="extraNetworksControlSortDirOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--sort-dir-icon"></i>
|
||||
</div>
|
||||
|
||||
|
||||
<small> </small>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_tree_view"
|
||||
class="extra-network-control--tree-view {tree_view_btn_extra_class}"
|
||||
title="Enable Tree View"
|
||||
onclick="extraNetworksControlTreeViewOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--tree-view-icon"></i>
|
||||
</div>
|
||||
<div
|
||||
id="{tabname}_{extra_networks_tabname}_extra_refresh"
|
||||
class="extra-network-control--refresh"
|
||||
title="Refresh page"
|
||||
onclick="extraNetworksControlRefreshOnClick(event, '{tabname}', '{extra_networks_tabname}');"
|
||||
>
|
||||
<i class="extra-network-control--icon extra-network-control--refresh-icon"></i>
|
||||
<div class="extra-network-controls--container">
|
||||
<div class="extra-network-controls--header">Refresh</div>
|
||||
<div class="extra-network-controls--buttons">
|
||||
<button class="extra-network-control extra-network-control--refresh" title="Refresh tab">
|
||||
<svg class="extra-network-control--icon" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path d="M 8 14 A 6 6 0 1 1 14 8 L 12 6.5 M 14 8 L 15 5.5 " />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra-network-content extra-network-content--container resize-grid">
|
||||
<div id="{tabname}_{extra_networks_tabname}_dirs_view_row" class="resize-grid--row"
|
||||
style="{dirs_view_row_style}" {dirs_view_row_data_attributes}>
|
||||
<div id="{tabname}_{extra_networks_tabname}_dirs_view_cell" class="resize-grid--cell" data-min-size="0px" style="flex: 1 1 0px;">
|
||||
<div class="extra-network-content extra-network-content--dirs-view styled-scrollbar">
|
||||
{dirs_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="{tabname}_{extra_networks_tabname}_main_row" class="resize-grid--row" style="{main_row_style}"
|
||||
{main_row_data_attributes}>
|
||||
<div id="{tabname}_{extra_networks_tabname}_tree_view_cell" class="resize-grid--cell"
|
||||
style="{tree_view_cell_style}" {tree_view_cell_data_attributes}>
|
||||
<div class="extra-network-content extra-network-content--tree-view">
|
||||
<div id='{tabname}_{extra_networks_tabname}_tree_list_loading_splash'
|
||||
class='extra-network-list-splash'>
|
||||
{tree_list_loading_splash_content}</div>
|
||||
<div id='{tabname}_{extra_networks_tabname}_tree_list_no_data_splash'
|
||||
class='extra-network-list-splash hidden'>{tree_list_no_data_splash_content}</div>
|
||||
<div id='{tabname}_{extra_networks_tabname}_tree_list_scroll_area'
|
||||
class='styled-scrollbar clusterize-scroll'>
|
||||
<div id='{tabname}_{extra_networks_tabname}_tree_list_content_area'
|
||||
class='extra-network-tree-content clusterize-content'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="{tabname}_{extra_networks_tabname}_card_view_cell" class="resize-grid--cell"
|
||||
style="{card_view_cell_style}" {card_view_cell_data_attributes}>
|
||||
<div class="extra-network-content extra-network-content--card-view extra-network-cards">
|
||||
<div id='{tabname}_{extra_networks_tabname}_card_list_loading_splash'
|
||||
class='extra-network-list-splash'>
|
||||
{card_list_loading_splash_content}</div>
|
||||
<div id='{tabname}_{extra_networks_tabname}_card_list_no_data_splash'
|
||||
class='extra-network-list-splash hidden'>{card_list_no_data_splash_content}
|
||||
</div>
|
||||
<div id='{tabname}_{extra_networks_tabname}_card_list_scroll_area'
|
||||
class='styled-scrollbar clusterize-scroll'>
|
||||
<div id='{tabname}_{extra_networks_tabname}_card_list_content_area'
|
||||
class='extra-network-card-content clusterize-content'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="{tabname}_{extra_networks_tabname}_dets_view_cell" class="resize-grid--cell"
|
||||
style="{dets_view_cell_style}" {dets_view_cell_data_attributes}>
|
||||
<div class="extra-network-content extra-network-content--dets-view styled-scrollbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{pane_content}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
<span data-filterable-item-text hidden>{search_terms}</span>
|
||||
<div class="tree-list-content {subclass}"
|
||||
type="button"
|
||||
onclick="extraNetworksTreeOnClick(event, '{tabname}', '{extra_networks_tabname}');{onclick_extra}"
|
||||
data-path="{data_path}"
|
||||
data-hash="{data_hash}"
|
||||
>
|
||||
<span class='tree-list-item-action tree-list-item-action--leading'>
|
||||
{action_list_item_action_leading}
|
||||
</span>
|
||||
<span class="tree-list-item-visual tree-list-item-visual--leading">
|
||||
{action_list_item_visual_leading}
|
||||
</span>
|
||||
<span class="tree-list-item-label tree-list-item-label--truncate">
|
||||
{action_list_item_label}
|
||||
</span>
|
||||
<span class="tree-list-item-visual tree-list-item-visual--trailing">
|
||||
{action_list_item_visual_trailing}
|
||||
</span>
|
||||
<span class="tree-list-item-action tree-list-item-action--trailing">
|
||||
{action_list_item_action_trailing}
|
||||
</span>
|
||||
</div>
|
||||
11
html/extra-networks-tree-row.html
Normal file
11
html/extra-networks-tree-row.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<div class="tree-list-item tree-list-item--{btn_type}" title={btn_title} {data_attributes}>
|
||||
{indent_spans}
|
||||
<button>
|
||||
<span data-filterable-item-text hidden>{search_terms}</span>
|
||||
<span class='tree-list-item-action tree-list-item-action--leading'>{action_list_item_action_leading}</span>
|
||||
<span class="tree-list-item-visual tree-list-item-visual--leading">{action_list_item_visual_leading}</span>
|
||||
<span class="tree-list-item-label tree-list-item-label--truncate">{action_list_item_label}</span>
|
||||
<span class="tree-list-item-visual tree-list-item-visual--trailing">{action_list_item_visual_trailing}</span>
|
||||
<span class="tree-list-item-action tree-list-item-action--trailing">{action_list_item_action_trailing}</span>
|
||||
</button>
|
||||
</div>
|
||||
622
javascript/clusterize.js
Normal file
622
javascript/clusterize.js
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
// Many operations can be lenghty. Try to limit their frequency by debouncing.
|
||||
const SCROLL_DEBOUNCE_TIME_MS = 50;
|
||||
const RESIZE_OBSERVER_DEBOUNCE_TIME_MS = 100; // should be <= refresh debounce time
|
||||
const ELEMENT_OBSERVER_DEBOUNCE_TIME_MS = 100;
|
||||
const REFRESH_DEBOUNCE_TIME_MS = 100;
|
||||
|
||||
class Clusterize {
|
||||
scroll_elem = null;
|
||||
content_elem = null;
|
||||
scroll_id = null;
|
||||
content_id = null;
|
||||
config = {
|
||||
block_height: null,
|
||||
block_width: null,
|
||||
cols_in_block: 1,
|
||||
cluster_height: null,
|
||||
cluster_width: null,
|
||||
is_mac: navigator.userAgent.toLowerCase().indexOf("mac") > -1,
|
||||
item_height: null,
|
||||
item_width: null,
|
||||
max_items: 0,
|
||||
max_rows: 0,
|
||||
rows_in_block: 50,
|
||||
rows_in_cluster: 50 * 5, // default is rows_in_block * blocks_in_cluster
|
||||
};
|
||||
options = {
|
||||
blocks_in_cluster: 5,
|
||||
tag: "div",
|
||||
no_data_class: "clusterize-no-data",
|
||||
no_data_html: "No Data",
|
||||
error_class: "clusterize-error",
|
||||
error_html: "Data Error",
|
||||
keep_parity: true,
|
||||
callbacks: {},
|
||||
};
|
||||
state = {
|
||||
cache: {},
|
||||
curr_cluster: 0,
|
||||
enabled: false,
|
||||
scroll_top: 0,
|
||||
setup_has_run: false,
|
||||
pointer_events_set: false,
|
||||
};
|
||||
#scroll_debounce_timer = 0;
|
||||
#refresh_debounce_timer = null;
|
||||
#resize_observer = null;
|
||||
#resize_observer_timer = null;
|
||||
#element_observer = null;
|
||||
#element_observer_timer = null;
|
||||
#on_scroll_bound;
|
||||
|
||||
constructor(args) {
|
||||
for (const option of Object.keys(this.options)) {
|
||||
if (keyExists(args, option)) {
|
||||
this.options[option] = args[option];
|
||||
}
|
||||
}
|
||||
|
||||
if (isNullOrUndefined(this.options.callbacks.initData)) {
|
||||
this.options.callbacks.initData = this.initDataDefaultCallback.bind(this);
|
||||
}
|
||||
if (isNullOrUndefined(this.options.callbacks.fetchData)) {
|
||||
this.options.callbacks.fetchData = this.fetchDataDefaultCallback.bind(this);
|
||||
}
|
||||
if (isNullOrUndefined(this.options.callbacks.sortData)) {
|
||||
this.options.callbacks.sortData = this.sortDataDefaultCallback.bind(this);
|
||||
}
|
||||
if (isNullOrUndefined(this.options.callbacks.filterData)) {
|
||||
this.options.callbacks.filterData = this.filterDataDefaultCallback.bind(this);
|
||||
}
|
||||
|
||||
this.scroll_elem = args["scrollId"] ? document.getElementById(args["scrollId"]) : args["scrollElem"];
|
||||
isElementThrowError(this.scroll_elem);
|
||||
this.scroll_id = this.scroll_elem.id;
|
||||
|
||||
this.content_elem = args["contentId"] ? document.getElementById(args["contentId"]) : args["contentElem"];
|
||||
isElementThrowError(this.content_elem);
|
||||
this.content_id = this.content_elem.id;
|
||||
|
||||
if (!this.content_elem.hasAttribute("tabindex")) {
|
||||
this.content_elem.setAttribute("tabindex", 0);
|
||||
}
|
||||
|
||||
this.state.scroll_top = this.scroll_elem.scrollTop;
|
||||
|
||||
this.config.max_items = args.max_items;
|
||||
|
||||
this.#on_scroll_bound = this.#onScroll.bind(this);
|
||||
}
|
||||
|
||||
// ==== PUBLIC FUNCTIONS ====
|
||||
enable(state) {
|
||||
// if no state is passed, we enable by default.
|
||||
this.state.enabled = state !== false;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
if (this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fixElementReferences();
|
||||
|
||||
// We use margins to control the scrollbar's size and float our content.
|
||||
this.content_elem.style.marginTop = "0px";
|
||||
this.content_elem.style.marginBottom = "0px";
|
||||
|
||||
await this.#insertToDOM();
|
||||
|
||||
this.#setupEvent("scroll", this.scroll_elem, this.#on_scroll_bound);
|
||||
this.#setupElementObservers();
|
||||
this.#setupResizeObservers();
|
||||
|
||||
this.state.setup_has_run = true;
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (!this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#html(this.#generateEmptyRow().join(""));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#teardownEvent("scroll", this.scroll_elem, this.#on_scroll_bound);
|
||||
this.#teardownElementObservers();
|
||||
this.#teardownResizeObservers();
|
||||
|
||||
this.#html(this.#generateEmptyRow().join(""));
|
||||
|
||||
this.state.setup_has_run = false;
|
||||
}
|
||||
|
||||
async refresh(force) {
|
||||
if (!this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh can be a longer operation so we want to debounce it to
|
||||
// avoid refreshing too often.
|
||||
clearTimeout(this.#refresh_debounce_timer);
|
||||
this.#refresh_debounce_timer = setTimeout(
|
||||
async () => {
|
||||
if (!isElement(this.content_elem.offsetParent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#recalculateDims() || force) {
|
||||
await this.update()
|
||||
}
|
||||
},
|
||||
REFRESH_DEBOUNCE_TIME_MS,
|
||||
)
|
||||
}
|
||||
|
||||
async update() {
|
||||
if (!this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state.scroll_top = this.scroll_elem.scrollTop;
|
||||
// fixes #39
|
||||
if (this.config.max_rows * this.config.item_height < this.state.scroll_top) {
|
||||
this.scroll_elem.scrollTop = 0;
|
||||
this.state.curr_cluster = 0;
|
||||
}
|
||||
|
||||
await this.#insertToDOM();
|
||||
}
|
||||
|
||||
getRowsAmount() {
|
||||
return this.config.max_rows;
|
||||
}
|
||||
|
||||
getScrollProgress() {
|
||||
return this.state.scroll_top / (this.config.max_rows * this.config.item_height) * 100 || 0;
|
||||
}
|
||||
|
||||
async setMaxItems(max_items) {
|
||||
/** Sets the new max number of items.
|
||||
*
|
||||
* This is used to control the scroll bar's length.
|
||||
*
|
||||
* Returns whether the number of max items changed.
|
||||
*/
|
||||
if (!this.state.setup_has_run || !this.state.enabled) {
|
||||
this.config.max_items = max_items;
|
||||
return this.config.max_items !== max_items;
|
||||
}
|
||||
|
||||
this.config.max_items = max_items;
|
||||
}
|
||||
|
||||
// ==== PRIVATE FUNCTIONS ====
|
||||
initDataDefaultCallback() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
|
||||
async initData() {
|
||||
if (!this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
return await this.options.callbacks.initData();
|
||||
}
|
||||
|
||||
fetchDataDefaultCallback() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async fetchData(idx_start, idx_end) {
|
||||
if (!this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return await this.options.callbacks.fetchData(idx_start, idx_end);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
sortDataDefaultCallback() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async sortData() {
|
||||
if (!this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#fixElementReferences();
|
||||
|
||||
// Sort is applied to the filtered data.
|
||||
await this.options.callbacks.sortData();
|
||||
this.#recalculateDims();
|
||||
await this.#insertToDOM();
|
||||
}
|
||||
|
||||
filterDataDefaultCallback() {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
async filterData() {
|
||||
if (!this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter is applied to entire dataset.
|
||||
const max_items = await this.options.callbacks.filterData();
|
||||
await this.setMaxItems(max_items);
|
||||
await this.refresh(true);
|
||||
await this.sortData();
|
||||
}
|
||||
|
||||
#exploreEnvironment(rows, cache) {
|
||||
this.options.content_tag = this.content_elem.tagName.toLowerCase();
|
||||
if (isNullOrUndefined(rows) || !rows.length) {
|
||||
return;
|
||||
}
|
||||
// Temporarily add one row so that we can calculate row dimensions.
|
||||
if (this.content_elem.children.length <= 1) {
|
||||
cache.data = this.#html(rows[0]);
|
||||
}
|
||||
if (!this.options.tag) {
|
||||
this.options.tag = this.content_elem.children[0].tagName.toLowerCase();
|
||||
}
|
||||
this.#recalculateDims();
|
||||
}
|
||||
|
||||
#recalculateDims() {
|
||||
const prev_config = JSON.stringify(this.config);
|
||||
|
||||
this.config.block_height = 0;
|
||||
this.config.block_width = 0;
|
||||
this.config.cluster_height = 0;
|
||||
this.config.cluster_width = 0;
|
||||
this.config.item_height = 0;
|
||||
this.config.item_width = 0;
|
||||
|
||||
if (!this.config.max_items) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first element that isn't one of our placeholder rows.
|
||||
const node = this.content_elem.querySelector(
|
||||
`:scope > :not(.clusterize-extra-row,.${this.options.no_data_class})`
|
||||
);
|
||||
if (!isElement(node)) {
|
||||
// dont attempt to compute dims if we have no data.
|
||||
return;
|
||||
}
|
||||
|
||||
const node_dims = getComputedDims(node);
|
||||
this.config.item_height = node_dims.height;
|
||||
this.config.item_width = node_dims.width;
|
||||
|
||||
// consider table's browser spacing
|
||||
if (this.options.tag === "tr" && getComputedProperty(this.content_elem, "borderCollapse") !== "collapse") {
|
||||
const spacing = parseInt(getComputedProperty(this.content_elem, "borderSpacing"), 10) || 0;
|
||||
this.config.item_height += spacing;
|
||||
this.config.item_width += spacing;
|
||||
}
|
||||
|
||||
// Update rows in block to match the number of elements that can fit in the view.
|
||||
const content_padding = getComputedPaddingDims(this.content_elem);
|
||||
const column_gap = parseFloat(getComputedProperty(this.content_elem, "column-gap"));
|
||||
const row_gap = parseFloat(getComputedProperty(this.content_elem, "row-gap"));
|
||||
if (isNumber(column_gap)) {
|
||||
this.config.item_width += column_gap;
|
||||
}
|
||||
if (isNumber(row_gap)) {
|
||||
this.config.item_height += row_gap;
|
||||
}
|
||||
|
||||
const inner_width = this.scroll_elem.clientWidth - content_padding.width;
|
||||
const inner_height = this.scroll_elem.clientHeight - content_padding.height;
|
||||
// Since we don't allow horizontal scrolling, we want to round down for columns.
|
||||
const cols_in_block = Math.floor(inner_width / this.config.item_width);
|
||||
// Round up for rows so that we don't cut rows off from the view.
|
||||
const rows_in_block = Math.ceil(inner_height / this.config.item_height);
|
||||
|
||||
// Always need at least 1 row/col in block
|
||||
this.config.cols_in_block = Math.max(1, cols_in_block);
|
||||
this.config.rows_in_block = Math.max(1, rows_in_block);
|
||||
|
||||
this.config.block_height = this.config.item_height * this.config.rows_in_block;
|
||||
this.config.block_width = this.config.item_width * this.config.cols_in_block;
|
||||
this.config.rows_in_cluster = this.options.blocks_in_cluster * this.config.rows_in_block;
|
||||
this.config.cluster_height = this.options.blocks_in_cluster * this.config.block_height;
|
||||
this.config.cluster_width = this.config.block_width;
|
||||
|
||||
this.config.max_rows = Math.ceil(this.config.max_items / this.config.cols_in_block);
|
||||
this.config.max_clusters = Math.ceil(this.config.max_rows / this.config.rows_in_cluster);
|
||||
|
||||
return prev_config !== JSON.stringify(this.config);
|
||||
}
|
||||
|
||||
#generateEmptyRow({is_error}={}) {
|
||||
const row = document.createElement(is_error ? "div" : this.options.tag);
|
||||
row.className = is_error ? this.options.error_class : this.options.no_data_class;
|
||||
if (this.options.tag === "tr") {
|
||||
const td = document.createElement("td");
|
||||
td.colSpan = 100;
|
||||
td.innerHTML = is_error ? this.options.error_html : this.options.no_data_html;
|
||||
row.appendChild(td);
|
||||
} else {
|
||||
row.innerHTML = is_error ? this.options.error_html : this.options.no_data_html;
|
||||
}
|
||||
return [row.outerHTML];
|
||||
}
|
||||
|
||||
#getClusterNum() {
|
||||
this.state.scroll_top = this.scroll_elem.scrollTop;
|
||||
const cluster_divider = this.config.cluster_height - this.config.block_height;
|
||||
const current_cluster = Math.floor(this.state.scroll_top / cluster_divider);
|
||||
return Math.min(current_cluster, this.config.max_clusters);
|
||||
}
|
||||
|
||||
async #generate() {
|
||||
const rows_start = Math.max(0, (this.config.rows_in_cluster - this.config.rows_in_block) * this.#getClusterNum());
|
||||
const rows_end = rows_start + this.config.rows_in_cluster;
|
||||
const top_offset = Math.max(0, rows_start * this.config.item_height);
|
||||
const bottom_offset = Math.max(0, (this.config.max_rows - rows_end) * this.config.item_height);
|
||||
const rows_above = top_offset < 1 ? rows_start + 1 : rows_start;
|
||||
|
||||
const idx_start = Math.max(0, rows_start * this.config.cols_in_block);
|
||||
const idx_end = Math.min(this.config.max_items, rows_end * this.config.cols_in_block);
|
||||
|
||||
let this_cluster_rows = await this.fetchData(idx_start, idx_end);
|
||||
if (!Array.isArray(this_cluster_rows) || !this_cluster_rows.length) {
|
||||
console.error(`Failed to fetch data for idx range (${idx_start},${idx_end})`);
|
||||
this_cluster_rows = [];
|
||||
}
|
||||
|
||||
if (this_cluster_rows.length < this.config.rows_in_block) {
|
||||
return {
|
||||
top_offset: 0,
|
||||
bottom_offset: 0,
|
||||
rows_above: 0,
|
||||
rows: this_cluster_rows.length ? this_cluster_rows : this.#generateEmptyRow({is_error: true}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
top_offset: top_offset,
|
||||
bottom_offset: bottom_offset,
|
||||
rows_above: rows_above,
|
||||
rows: this_cluster_rows,
|
||||
};
|
||||
}
|
||||
|
||||
async #insertToDOM() {
|
||||
if (!this.config.cluster_height || !this.config.cluster_width) {
|
||||
// We need to fetch a single item so that we can calculate the dimensions
|
||||
// for our list.
|
||||
const rows = await this.fetchData(0, 1);
|
||||
if (!Array.isArray(rows) || !rows.length) {
|
||||
// This implies there is no data for this list. Not an error.
|
||||
// Errors should be handled in the fetchData callback, not here.
|
||||
this.#html(this.#generateEmptyRow().join(""));
|
||||
return;
|
||||
} else {
|
||||
this.#html(rows.join(""));
|
||||
this.#exploreEnvironment(rows, this.state.cache);
|
||||
// Remove the temporary item from the data since we calculated its size.
|
||||
this.#html(this.#generateEmptyRow().join(""));
|
||||
}
|
||||
}
|
||||
|
||||
const data = await this.#generate();
|
||||
let this_cluster_rows = [];
|
||||
for (let i = 0; i < data.rows.length; i += this.config.cols_in_block) {
|
||||
const new_row = data.rows.slice(i, i + this.config.cols_in_block).join("");
|
||||
this_cluster_rows.push(new_row);
|
||||
}
|
||||
this_cluster_rows = this_cluster_rows.join("");
|
||||
const this_cluster_content_changed = this.#checkChanges("data", this_cluster_rows, this.state.cache);
|
||||
const top_offset_changed = this.#checkChanges("top", data.top_offset, this.state.cache);
|
||||
const only_bottom_offset_changed = this.#checkChanges("bottom", data.bottom_offset, this.state.cache);
|
||||
const layout = [];
|
||||
|
||||
if (this_cluster_content_changed || top_offset_changed) {
|
||||
if (this.options.callbacks.clusterWillChange) {
|
||||
this.options.callbacks.clusterWillChange();
|
||||
}
|
||||
|
||||
if (data.top_offset && this.options.keep_parity) {
|
||||
layout.push(this.#renderExtraTag("keep-parity"));
|
||||
}
|
||||
layout.push(this_cluster_rows)
|
||||
this.#html(layout.join(""));
|
||||
if (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}`;
|
||||
if (this.options.callbacks.clusterChanged) {
|
||||
this.options.callbacks.clusterChanged();
|
||||
}
|
||||
}
|
||||
// Update the margins to fix scrollbar position.
|
||||
this.content_elem.style.marginTop = `${data.top_offset}px`;
|
||||
this.content_elem.style.marginBottom = `${data.bottom_offset}px`;
|
||||
}
|
||||
|
||||
#html(data) {
|
||||
this.content_elem.innerHTML = data;
|
||||
|
||||
// Parse items flagged as containing Shadow DOM entries.
|
||||
convertElementShadowDOM(this.content_elem, "[data-parse-as-shadow-dom]");
|
||||
|
||||
return this.content_elem.innerHTML;
|
||||
}
|
||||
|
||||
#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(" ");
|
||||
if (isNumber(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.config.is_mac) {
|
||||
if (!this.state.pointer_events_set) {
|
||||
this.content_elem.style.pointerEvents = "none";
|
||||
this.state.pointer_events_set = true;
|
||||
clearTimeout(this.#scroll_debounce_timer);
|
||||
this.#scroll_debounce_timer = setTimeout(() => {
|
||||
this.content_elem.style.pointerEvents = "auto";
|
||||
this.state.pointer_events_set = false;
|
||||
}, SCROLL_DEBOUNCE_TIME_MS);
|
||||
}
|
||||
}
|
||||
if (this.state.curr_cluster !== (this.state.curr_cluster = this.#getClusterNum())) {
|
||||
await this.#insertToDOM();
|
||||
}
|
||||
if (this.options.callbacks.scrollingProgress) {
|
||||
this.options.callbacks.scrollingProgress(this.getScrollProgress());
|
||||
}
|
||||
}
|
||||
|
||||
async #onResize() {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
#fixElementReferences() {
|
||||
if (!isElement(this.scroll_elem) || !isElement(this.content_elem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Element is already in DOM. Don't need to do anything.
|
||||
if (isElement(this.content_elem.offsetParent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If association for elements is broken, replace them with instance version.
|
||||
document.getElementById(this.scroll_id).replaceWith(this.scroll_elem);
|
||||
}
|
||||
|
||||
#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, listener, false);
|
||||
} else {
|
||||
return elem.attachEvent(`on${type}`, listener);
|
||||
}
|
||||
}
|
||||
|
||||
#teardownEvent(type, elem, listener) {
|
||||
if (elem.removeEventListener) {
|
||||
return elem.removeEventListener(type, listener, false);
|
||||
} else {
|
||||
return elem.detachEvent(`on${type}`, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
667
javascript/extraNetworksClusterize.js
Normal file
667
javascript/extraNetworksClusterize.js
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
// Prevent eslint errors on functions defined in other files.
|
||||
/*global
|
||||
Clusterize,
|
||||
getValueThrowError,
|
||||
INT_COLLATOR,
|
||||
STR_COLLATOR,
|
||||
LRUCache,
|
||||
isString,
|
||||
isNullOrUndefined,
|
||||
isNullOrUndefinedLogError,
|
||||
isElement,
|
||||
isElementLogError,
|
||||
keyExistsLogError,
|
||||
htmlStringToElement,
|
||||
convertElementShadowDOM,
|
||||
*/
|
||||
/*eslint no-undef: "error"*/
|
||||
|
||||
// number of list html items to store in cache.
|
||||
const EXTRA_NETWORKS_CLUSTERIZE_LRU_CACHE_SIZE = 1000;
|
||||
|
||||
class NotImplementedError extends Error {
|
||||
constructor(...params) {
|
||||
super(...params);
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, NotImplementedError);
|
||||
}
|
||||
|
||||
this.name = "NotImplementedError";
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraNetworksClusterize extends Clusterize {
|
||||
data_obj = {};
|
||||
data_obj_keys_sorted = [];
|
||||
lru = null;
|
||||
sort_reverse = false;
|
||||
default_sort_fn = this.sortByDivId;
|
||||
sort_fn = this.default_sort_fn;
|
||||
tabname = "";
|
||||
extra_networks_tabname = "";
|
||||
initial_load = false;
|
||||
|
||||
// Override base class defaults
|
||||
default_sort_mode_str = "divId";
|
||||
default_sort_dir_str = "ascending";
|
||||
default_filter_str = "";
|
||||
sort_mode_str = this.default_sort_mode_str;
|
||||
sort_dir_str = this.default_sort_dir_str;
|
||||
filter_str = this.default_filter_str;
|
||||
directory_filters = {};
|
||||
|
||||
constructor(args) {
|
||||
super(args);
|
||||
this.tabname = getValueThrowError(args, "tabname");
|
||||
this.extra_networks_tabname = getValueThrowError(args, "extra_networks_tabname");
|
||||
}
|
||||
|
||||
sortByDivId(data) {
|
||||
/** Sort data_obj keys (div_id) as numbers. */
|
||||
return Object.keys(data).sort(INT_COLLATOR.compare);
|
||||
}
|
||||
|
||||
async reinitData() {
|
||||
await this.initData();
|
||||
// can't use super class' sort since it relies on setup being run first.
|
||||
// but we do need to make sure to sort the new data before continuing.
|
||||
const max_items = Object.keys(this.data_obj).filter(k => this.data_obj[k].visible).length;
|
||||
await this.setMaxItems(max_items);
|
||||
await this.refresh(true);
|
||||
await this.options.callbacks.sortData();
|
||||
}
|
||||
|
||||
async setup() {
|
||||
if (this.state.setup_has_run || !this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.lru instanceof LRUCache) {
|
||||
this.lru.clear();
|
||||
} else {
|
||||
this.lru = new LRUCache(EXTRA_NETWORKS_CLUSTERIZE_LRU_CACHE_SIZE);
|
||||
}
|
||||
|
||||
await this.reinitData();
|
||||
|
||||
if (this.state.enabled) {
|
||||
await super.setup();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.initial_load = false;
|
||||
this.data_obj = {};
|
||||
this.data_obj_keys_sorted = [];
|
||||
if (this.lru instanceof LRUCache) {
|
||||
this.lru.destroy();
|
||||
this.lru = null;
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.initial_load = false;
|
||||
this.data_obj = {};
|
||||
this.data_obj_keys_sorted = [];
|
||||
if (this.lru instanceof LRUCache) {
|
||||
this.lru.clear();
|
||||
}
|
||||
super.clear();
|
||||
}
|
||||
|
||||
async load(force_init_data) {
|
||||
if (!this.state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initial_load = true;
|
||||
if (!this.state.setup_has_run) {
|
||||
await this.setup();
|
||||
} else if (force_init_data) {
|
||||
await this.reinitData();
|
||||
} else {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
setSortMode(sort_mode_str) {
|
||||
if (this.sort_mode_str === sort_mode_str) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sort_mode_str = sort_mode_str;
|
||||
this.sortData();
|
||||
}
|
||||
|
||||
setSortDir(sort_dir_str) {
|
||||
const reverse = (sort_dir_str === "descending");
|
||||
if (this.sort_reverse === reverse) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sort_dir_str = sort_dir_str;
|
||||
this.sort_reverse = reverse;
|
||||
this.sortData();
|
||||
}
|
||||
|
||||
setFilterStr(filter_str) {
|
||||
if (isString(filter_str) && this.filter_str !== filter_str) {
|
||||
this.filter_str = filter_str;
|
||||
} else if (isNullOrUndefined(filter_str)) {
|
||||
this.filter_str = this.default_filter_str;
|
||||
}
|
||||
this.filterData();
|
||||
}
|
||||
|
||||
setDirectoryFilters(filters) {
|
||||
if (isNullOrUndefined(filters)) {
|
||||
this.directory_filters = {};
|
||||
return;
|
||||
}
|
||||
this.directory_filters = JSON.parse(JSON.stringify(filters));
|
||||
}
|
||||
|
||||
addDirectoryFilter(div_id, filter_str, recurse) {
|
||||
this.directory_filters[div_id] = {filter_str: filter_str, recurse: recurse};
|
||||
}
|
||||
|
||||
removeDirectoryFilter(div_id) {
|
||||
delete this.directory_filters[div_id];
|
||||
}
|
||||
|
||||
clearDirectoryFilters({excluded_div_ids} = {}) {
|
||||
if (isString(excluded_div_ids)) {
|
||||
excluded_div_ids = [excluded_div_ids];
|
||||
}
|
||||
|
||||
if (!Array.isArray(excluded_div_ids)) {
|
||||
excluded_div_ids = [];
|
||||
}
|
||||
|
||||
for (const div_id of Object.keys(this.directory_filters)) {
|
||||
if (excluded_div_ids.includes(div_id)) {
|
||||
continue;
|
||||
}
|
||||
delete this.directory_filters[div_id];
|
||||
}
|
||||
}
|
||||
|
||||
getDirectoryFilters() {
|
||||
return this.directory_filters;
|
||||
}
|
||||
|
||||
async initDataDefaultCallback() {
|
||||
throw new NotImplementedError();
|
||||
}
|
||||
|
||||
idxRangeToDivIds(idx_start, idx_end) {
|
||||
return this.data_obj_keys_sorted.slice(idx_start, idx_end);
|
||||
}
|
||||
|
||||
async fetchDivIds(div_ids) {
|
||||
if (isNullOrUndefinedLogError(this.lru)) {
|
||||
return [];
|
||||
}
|
||||
if (Object.keys(this.data_obj).length === 0) {
|
||||
return [];
|
||||
}
|
||||
const lru_keys = Array.from(this.lru.cache.keys());
|
||||
const cached_div_ids = div_ids.filter(x => lru_keys.includes(x));
|
||||
const missing_div_ids = div_ids.filter(x => !lru_keys.includes(x));
|
||||
|
||||
const data = {};
|
||||
// Fetch any div IDs not in the LRU Cache using our callback.
|
||||
if (missing_div_ids.length !== 0) {
|
||||
const fetched_data = await this.options.callbacks.fetchData(missing_div_ids);
|
||||
if (Object.keys(fetched_data).length !== missing_div_ids.length) {
|
||||
// expected data. got nothing.
|
||||
return {};
|
||||
}
|
||||
Object.assign(data, fetched_data);
|
||||
}
|
||||
|
||||
// Now load any cached IDs from the LRU Cache
|
||||
for (const div_id of cached_div_ids) {
|
||||
if (!keyExistsLogError(this.data_obj, div_id)) {
|
||||
continue;
|
||||
}
|
||||
if (this.data_obj[div_id].visible) {
|
||||
data[div_id] = this.lru.get(div_id);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async fetchDataDefaultCallback() {
|
||||
throw new NotImplementedError();
|
||||
}
|
||||
|
||||
async sortDataDefaultCallback() {
|
||||
// we want to apply the sort to the visible items only.
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(this.data_obj).filter(([k, v]) => v.visible)
|
||||
);
|
||||
this.data_obj_keys_sorted = this.sort_fn(filtered);
|
||||
if (this.sort_reverse) {
|
||||
this.data_obj_keys_sorted = this.data_obj_keys_sorted.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
async filterDataDefaultCallback() {
|
||||
throw new NotImplementedError();
|
||||
}
|
||||
|
||||
updateHtml(elem, new_html) {
|
||||
const existing = this.lru.get(String(elem.dataset.divId));
|
||||
if (new_html) {
|
||||
if (existing === new_html) {
|
||||
return;
|
||||
}
|
||||
const parsed_html = htmlStringToElement(new_html);
|
||||
convertElementShadowDOM(parsed_html, "[data-parse-as-shadow-dom]");
|
||||
|
||||
// replace the element in DOM with our new element
|
||||
elem.replaceWith(parsed_html);
|
||||
|
||||
// update the internal cache with the new html
|
||||
this.lru.set(String(elem.dataset.divId), new_html);
|
||||
} else {
|
||||
if (existing === elem.outerHTML) {
|
||||
return;
|
||||
}
|
||||
this.lru.set(String(elem.dataset.divId), elem.outerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraNetworksClusterizeTreeList extends ExtraNetworksClusterize {
|
||||
prev_selected_div_id = null;
|
||||
|
||||
constructor(args) {
|
||||
super({...args});
|
||||
this.selected_div_ids = new Set();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.prev_selected_div_id = null;
|
||||
this.selected_div_ids.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
setRowSelected(elem) {
|
||||
if (!isElement(elem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateHtml(elem);
|
||||
this.selected_div_ids.add(elem.dataset.divId);
|
||||
this.prev_selected_div_id = elem.dataset.divId;
|
||||
}
|
||||
|
||||
setRowDeselected(elem) {
|
||||
if (!isElement(elem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateHtml(elem);
|
||||
this.selected_div_ids.delete(elem.dataset.divId);
|
||||
this.prev_selected_div_id = null;
|
||||
}
|
||||
|
||||
clearSelectedRows({excluded_div_ids} = {}) {
|
||||
if (isString(excluded_div_ids)) {
|
||||
excluded_div_ids = [excluded_div_ids];
|
||||
}
|
||||
|
||||
if (!Array.isArray(excluded_div_ids)) {
|
||||
excluded_div_ids = [];
|
||||
}
|
||||
|
||||
this.selected_div_ids.clear();
|
||||
for (const div_id of excluded_div_ids) {
|
||||
this.selected_div_ids.add(div_id);
|
||||
}
|
||||
if (!excluded_div_ids.includes(this.prev_selected_div_id)) {
|
||||
this.prev_selected_div_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
getMaxRowWidth() {
|
||||
/** Calculates the width of the widest row in the list. */
|
||||
if (!this.state.enabled) {
|
||||
// Inactive list is not displayed on screen. Can't calculate size.
|
||||
return;
|
||||
}
|
||||
if (this.content_elem.children.length === 0) {
|
||||
// If there is no data then just skip.
|
||||
return;
|
||||
}
|
||||
|
||||
let max_width = 0;
|
||||
for (let i = 0; i < this.content_elem.children.length; i += this.config.cols_in_block) {
|
||||
let row_width = 0;
|
||||
for (let j = 0; j < this.config.cols_in_block; j++) {
|
||||
const child = this.content_elem.children[i + j];
|
||||
if (!(child.classList.contains("tree-list-item"))) {
|
||||
continue;
|
||||
}
|
||||
// Child first element is the indent div. Just use offset for this
|
||||
// since we do some overlapping with ::after in CSS.
|
||||
row_width += child.children[0].offsetWidth;
|
||||
// Button is second element. We want entire scroll width of this one.
|
||||
// But first we need to allow it to shrink to content.
|
||||
const prev_css_text = child.children[1].cssText;
|
||||
child.children[1].style.flex = "0 1 auto";
|
||||
row_width += child.children[1].scrollWidth;
|
||||
// Add the button label's overflow to the width.
|
||||
const lbl = child.querySelector(".tree-list-item-label");
|
||||
row_width += lbl.scrollWidth - lbl.offsetWidth;
|
||||
// Revert changes to element style.
|
||||
if (!prev_css_text) {
|
||||
child.children[1].removeAttribute("style");
|
||||
} else {
|
||||
child.children[1].cssText = prev_css_text;
|
||||
}
|
||||
}
|
||||
max_width = Math.max(row_width, max_width);
|
||||
}
|
||||
if (max_width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Adds the scroll_elem's scrollbar and padding to the result.
|
||||
// If scrollbar isn't visible, then only the element border/padding is added.
|
||||
max_width += this.scroll_elem.offsetWidth - this.content_elem.offsetWidth;
|
||||
return max_width;
|
||||
}
|
||||
|
||||
async expandAllRows(div_id) {
|
||||
/** Recursively expands all directories below the passed div_id. */
|
||||
if (!keyExistsLogError(this.data_obj, div_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _expand = (parent_id) => {
|
||||
const this_obj = this.data_obj[parent_id];
|
||||
this_obj.visible = true;
|
||||
this_obj.expanded = true;
|
||||
for (const child_id of this_obj.children) {
|
||||
_expand(child_id);
|
||||
}
|
||||
};
|
||||
|
||||
this.data_obj[div_id].expanded = true;
|
||||
for (const child_id of this.data_obj[div_id].children) {
|
||||
_expand(child_id);
|
||||
}
|
||||
|
||||
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
|
||||
await this.setMaxItems(new_len);
|
||||
await this.refresh(true);
|
||||
await this.sortData();
|
||||
}
|
||||
|
||||
async collapseAllRows(div_id) {
|
||||
/** Recursively collapses all directories below the passed div_id. */
|
||||
if (!keyExistsLogError(this.data_obj, div_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _collapse = (parent_id) => {
|
||||
const this_obj = this.data_obj[parent_id];
|
||||
this_obj.visible = false;
|
||||
this_obj.expanded = false;
|
||||
for (const child_id of this_obj.children) {
|
||||
_collapse(child_id);
|
||||
}
|
||||
};
|
||||
|
||||
this.data_obj[div_id].expanded = false;
|
||||
for (const child_id of this.data_obj[div_id].children) {
|
||||
_collapse(child_id);
|
||||
}
|
||||
|
||||
// Deselect current selected div id if it was just hidden.
|
||||
if (this.selected_div_ids.has(div_id) && !this.data_obj[div_id].visible) {
|
||||
this.selected_div_ids.delete(div_id);
|
||||
if (this.prev_selected_div_id === div_id) {
|
||||
this.prev_selected_div_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
|
||||
await this.setMaxItems(new_len);
|
||||
await this.refresh(true);
|
||||
await this.sortData();
|
||||
}
|
||||
|
||||
getChildrenDivIds(div_id, {recurse} = {}) {
|
||||
const res = JSON.parse(JSON.stringify(this.data_obj[div_id].children));
|
||||
if (recurse === true) {
|
||||
for (const child_id of this.data_obj[div_id].children) {
|
||||
res.push(...this.getChildrenDivIds(child_id, {recurse: recurse}));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async toggleRowExpanded(div_id) {
|
||||
/** Toggles a row between expanded and collapses states. */
|
||||
if (!keyExistsLogError(this.data_obj, div_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle state
|
||||
this.data_obj[div_id].expanded = !this.data_obj[div_id].expanded;
|
||||
|
||||
const _set_visibility = (parent_id, visible) => {
|
||||
const this_obj = this.data_obj[parent_id];
|
||||
this_obj.visible = visible;
|
||||
for (const child_id of this_obj.children) {
|
||||
_set_visibility(child_id, visible && this_obj.expanded);
|
||||
}
|
||||
};
|
||||
|
||||
for (const child_id of this.data_obj[div_id].children) {
|
||||
_set_visibility(child_id, this.data_obj[div_id].expanded);
|
||||
}
|
||||
|
||||
// Deselect current selected div id if it was just hidden.
|
||||
if (this.selected_div_ids.has(div_id) && !this.data_obj[div_id].visible) {
|
||||
this.selected_div_ids.delete(div_id);
|
||||
if (this.prev_selected_div_id === div_id) {
|
||||
this.prev_selected_div_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
const new_len = Object.values(this.data_obj).filter(v => v.visible).length;
|
||||
await this.setMaxItems(new_len);
|
||||
await this.refresh(true);
|
||||
await this.sortData();
|
||||
}
|
||||
|
||||
async initData() {
|
||||
/*Expects an object like the following:
|
||||
{
|
||||
parent: null or div_id,
|
||||
children: array of div_id's,
|
||||
visible: bool,
|
||||
expanded: bool,
|
||||
}
|
||||
*/
|
||||
this.data_obj = await this.options.callbacks.initData();
|
||||
}
|
||||
|
||||
async fetchData(idx_start, idx_end) {
|
||||
if (!this.state.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Object.keys(this.data_obj).length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await this.fetchDivIds(this.idxRangeToDivIds(idx_start, idx_end));
|
||||
const data_ids_sorted = Object.keys(data).sort((a, b) => {
|
||||
return this.data_obj_keys_sorted.indexOf(a) - this.data_obj_keys_sorted.indexOf(b);
|
||||
});
|
||||
|
||||
const res = [];
|
||||
for (const div_id of data_ids_sorted) {
|
||||
if (!keyExistsLogError(this.data_obj, div_id)) {
|
||||
continue;
|
||||
}
|
||||
const html_str = data[div_id];
|
||||
const elem = isElement(html_str) ? html_str : htmlStringToElement(html_str);
|
||||
|
||||
// Roots come expanded by default. Need to delete if it exists.
|
||||
delete elem.dataset.expanded;
|
||||
if (this.data_obj[div_id].expanded) {
|
||||
elem.dataset.expanded = "";
|
||||
}
|
||||
|
||||
delete elem.dataset.selected;
|
||||
if (this.selected_div_ids.has(div_id)) {
|
||||
elem.dataset.selected = "";
|
||||
}
|
||||
|
||||
this.lru.set(String(div_id), elem.outerHTML);
|
||||
res.push(elem.outerHTML);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async filterDataDefaultCallback() {
|
||||
// just return the number of visible objects in our data.
|
||||
return Object.values(this.data_obj).filter(v => v.visible).length;
|
||||
}
|
||||
}
|
||||
|
||||
class ExtraNetworksClusterizeCardList extends ExtraNetworksClusterize {
|
||||
constructor(args) {
|
||||
super({...args});
|
||||
}
|
||||
|
||||
sortByPath(data) {
|
||||
return Object.keys(data).sort((a, b) => {
|
||||
return INT_COLLATOR.compare(data[a].sort_path, data[b].sort_path);
|
||||
});
|
||||
}
|
||||
|
||||
sortByName(data) {
|
||||
return Object.keys(data).sort((a, b) => {
|
||||
return INT_COLLATOR.compare(data[a].sort_name, data[b].sort_name);
|
||||
});
|
||||
}
|
||||
|
||||
sortByDateCreated(data) {
|
||||
return Object.keys(data).sort((a, b) => {
|
||||
return INT_COLLATOR.compare(data[a].sort_date_created, data[b].sort_date_created);
|
||||
});
|
||||
}
|
||||
|
||||
sortByDateModified(data) {
|
||||
return Object.keys(data).sort((a, b) => {
|
||||
return INT_COLLATOR.compare(data[a].sort_date_modified, data[b].sort_date_modified);
|
||||
});
|
||||
}
|
||||
|
||||
async initData() {
|
||||
/*Expects an object like the following:
|
||||
{
|
||||
search_keys: array of strings,
|
||||
search_only: bool,
|
||||
sort_<mode>: string, (for various sort modes)
|
||||
}
|
||||
*/
|
||||
this.data_obj = await this.options.callbacks.initData();
|
||||
}
|
||||
|
||||
async fetchData(idx_start, idx_end) {
|
||||
if (!this.state.enabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await this.fetchDivIds(this.idxRangeToDivIds(idx_start, idx_end));
|
||||
const data_ids_sorted = Object.keys(data).sort((a, b) => {
|
||||
return this.data_obj_keys_sorted.indexOf(a) - this.data_obj_keys_sorted.indexOf(b);
|
||||
});
|
||||
|
||||
const res = [];
|
||||
for (const div_id of data_ids_sorted) {
|
||||
res.push(data[div_id]);
|
||||
this.lru.set(div_id, data[div_id]);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async sortData() {
|
||||
switch (this.sort_mode_str) {
|
||||
case "name":
|
||||
this.sort_fn = this.sortByName;
|
||||
break;
|
||||
case "path":
|
||||
this.sort_fn = this.sortByPath;
|
||||
break;
|
||||
case "date_created":
|
||||
this.sort_fn = this.sortByDateCreated;
|
||||
break;
|
||||
case "date_modified":
|
||||
this.sort_fn = this.sortByDateModified;
|
||||
break;
|
||||
default:
|
||||
this.sort_fn = this.default_sort_fn;
|
||||
break;
|
||||
}
|
||||
await super.sortData();
|
||||
}
|
||||
|
||||
async filterDataDefaultCallback() {
|
||||
/** Filters data by a string and returns number of items after filter. */
|
||||
let n_visible = 0;
|
||||
|
||||
for (const [div_id, v] of Object.entries(this.data_obj)) {
|
||||
let visible = true;
|
||||
|
||||
// Apply the directory filters.
|
||||
if (!Object.keys(this.directory_filters).length) {
|
||||
v.visible = true;
|
||||
} else {
|
||||
v.visible = Object.values(this.directory_filters).some((filter) => {
|
||||
if (filter.recurse) {
|
||||
return v.rel_parent_dir.startsWith(filter.filter_str);
|
||||
} else {
|
||||
return v.rel_parent_dir === filter.filter_str;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!v.visible) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Narrow the filtered items based on the search string.
|
||||
// Custom filter for items marked search_only=true.
|
||||
if (v.search_only) {
|
||||
if (Object.keys(this.directory_filters).length || this.filter_str.length >= 4) {
|
||||
visible = v.search_terms.toLowerCase().indexOf(this.filter_str.toLowerCase()) !== -1;
|
||||
} else {
|
||||
visible = false;
|
||||
}
|
||||
} else {
|
||||
// All other filters treated case insensitive.
|
||||
visible = v.search_terms.toLowerCase().indexOf(this.filter_str.toLowerCase()) !== -1;
|
||||
}
|
||||
|
||||
this.data_obj[div_id].visible = visible;
|
||||
if (visible) {
|
||||
n_visible++;
|
||||
}
|
||||
}
|
||||
return n_visible;
|
||||
}
|
||||
}
|
||||
59
javascript/lru_cache.js
Normal file
59
javascript/lru_cache.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Prevent eslint errors on functions defined in other files.
|
||||
/*global
|
||||
isNumberThrowError,
|
||||
isNullOrUndefined,
|
||||
*/
|
||||
/*eslint no-undef: "error"*/
|
||||
|
||||
const LRU_CACHE_MAX_ITEMS_DEFAULT = 250;
|
||||
class LRUCache {
|
||||
/** Least Recently Used cache implementation.
|
||||
*
|
||||
* Source: https://stackoverflow.com/a/46432113
|
||||
*/
|
||||
constructor(max = LRU_CACHE_MAX_ITEMS_DEFAULT) {
|
||||
isNumberThrowError(max);
|
||||
|
||||
this.max = max;
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clear();
|
||||
this.cache = null;
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
get(key) {
|
||||
let item = this.cache.get(key);
|
||||
if (!isNullOrUndefined(item)) {
|
||||
this.cache.delete(key);
|
||||
this.cache.set(key, item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
set(key, val) {
|
||||
if (this.cache.has(key)) {
|
||||
this.cache.delete(key);
|
||||
} else if (this.cache.size === this.max) {
|
||||
this.cache.delete(this.first());
|
||||
}
|
||||
this.cache.set(key, val);
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
|
||||
first() {
|
||||
return this.cache.keys().next().value;
|
||||
}
|
||||
}
|
||||
1300
javascript/resizeGrid.js
Normal file
1300
javascript/resizeGrid.js
Normal file
File diff suppressed because it is too large
Load diff
178
javascript/resizeGridExample.html
Normal file
178
javascript/resizeGridExample.html
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
background: #333333;
|
||||
margin: 50px;
|
||||
}
|
||||
|
||||
.my-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cell_0_0 {
|
||||
background: #ffb3ba;
|
||||
}
|
||||
|
||||
#cell_0_1 {
|
||||
background: #ffdfba;
|
||||
}
|
||||
|
||||
#cell_1_0 {
|
||||
background: #baffc9;
|
||||
}
|
||||
|
||||
#cell_1_1 {
|
||||
background: #bae1ff;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.resizing {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
body.resizing * {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
body.resizing .resize-grid--handle {
|
||||
pointer-events: initial !important;
|
||||
}
|
||||
|
||||
body.resizing.resize-grid-col {
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
body.resizing.resize-grid-row {
|
||||
cursor: row-resize !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 50vw;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
border: 1px solid white;
|
||||
padding: 10px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.resize-grid,
|
||||
.resize-grid--row,
|
||||
.resize-grid--col,
|
||||
.resize-grid--cell {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
gap: 0 !important;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.resize-grid {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resize-grid--row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.resize-grid--col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.resize-grid--handle {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.resize-grid--handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.resize-grid--row-handle {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.resize-grid--row-handle::after {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-top: 1px dashed white;
|
||||
}
|
||||
|
||||
.resize-grid--col-handle {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.resize-grid--col-handle::after {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-left: 1px dashed white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="resize-grid">
|
||||
<div class="resize-grid--row" style="flex: 1 0 50px;" data-min-size="25px">
|
||||
<div class="resize-grid--cell" style="flex: 0 0 50px;" data-min-size="25px">
|
||||
<div id="cell_0_0" class="my-content">0</div>
|
||||
</div>
|
||||
<div class="resize-grid--cell" style="flex: 1 0 50px;" data-min-size="25px">
|
||||
<div id="cell_0_1" class="my-content">1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resize-grid--row" style="flex: 0 0 50px;" data-min-size="25px">
|
||||
<div class="resize-grid--cell" style="flex: 1 0 50px;" data-min-size="25px">
|
||||
<div id="cell_1_0" class="my-content">2</div>
|
||||
</div>
|
||||
<div class="resize-grid--cell" style="flex: 0 0 50px;" data-min-size="25px">
|
||||
<div id="cell_1_1" class="my-content">3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="toggle-button" data-grid-elem-id="cell_0_0">HIDE cell_0_0</button>
|
||||
<button class="toggle-button" data-grid-elem-id="cell_0_1">HIDE cell_0_1</button>
|
||||
<button class="toggle-button" data-grid-elem-id="cell_1_0">HIDE cell_1_0</button>
|
||||
<button class="toggle-button" data-grid-elem-id="cell_1_1">HIDE cell_1_1</button>
|
||||
<script type="text/javascript" src="utils.js"></script>
|
||||
<script type="text/javascript" src="resizeGrid.js"></script>
|
||||
<script type="text/javascript">
|
||||
const grid_elem = document.querySelector(".resize-grid");
|
||||
const grid = resizeGridSetup(grid_elem);
|
||||
const btns = document.querySelectorAll(".toggle-button");
|
||||
btns.forEach(btn => {
|
||||
btn.addEventListener("click", onToggleButton);
|
||||
});
|
||||
function onToggleButton(event) {
|
||||
const btn = event.target.closest("button");
|
||||
const id = btn.dataset.gridElemId;
|
||||
const grid_elem = document.querySelector(`#${id}`);
|
||||
grid.toggle({ elem: grid_elem });
|
||||
btn.textContent = btn.textContent === `HIDE ${id}` ? `SHOW ${id}` : `HIDE ${id}`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
(function() {
|
||||
const GRADIO_MIN_WIDTH = 320;
|
||||
const PAD = 16;
|
||||
const DEBOUNCE_TIME = 100;
|
||||
const DOUBLE_TAP_DELAY = 200; //ms
|
||||
const DEBOUNCE_TIME_MS = 250;
|
||||
const DOUBLE_TAP_DELAY_MS = 250;
|
||||
|
||||
const R = {
|
||||
tracking: false,
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
let parents = [];
|
||||
|
||||
function setLeftColGridTemplate(el, width) {
|
||||
el.style.gridTemplateColumns = `${width}px 16px 1fr`;
|
||||
el.style.gridTemplateColumns = `${width}px ${PAD}px 1fr`;
|
||||
}
|
||||
|
||||
function displayResizeHandle(parent) {
|
||||
|
|
@ -58,6 +58,9 @@
|
|||
evt.stopPropagation();
|
||||
|
||||
parent.style.gridTemplateColumns = parent.style.originalGridTemplateColumns;
|
||||
|
||||
// Fire a custom event so user can perform additional tasks on double click.
|
||||
parent.dispatchEvent(new CustomEvent("resizeHandleDblClick", {bubbles: true}));
|
||||
}
|
||||
|
||||
const leftCol = parent.firstElementChild;
|
||||
|
|
@ -101,7 +104,7 @@
|
|||
if (evt.changedTouches.length !== 1) return;
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
if (R.lastTapTime && currentTime - R.lastTapTime <= DOUBLE_TAP_DELAY) {
|
||||
if (R.lastTapTime && currentTime - R.lastTapTime <= DOUBLE_TAP_DELAY_MS) {
|
||||
onDoubleClick(evt);
|
||||
return;
|
||||
}
|
||||
|
|
@ -152,7 +155,13 @@
|
|||
} else {
|
||||
delta = R.screenX - evt.changedTouches[0].screenX;
|
||||
}
|
||||
const leftColWidth = Math.max(Math.min(R.leftColStartWidth - delta, R.parent.offsetWidth - R.parent.minRightColWidth - PAD), R.parent.minLeftColWidth);
|
||||
const leftColWidth = Math.max(
|
||||
Math.min(
|
||||
R.leftColStartWidth - delta,
|
||||
R.parent.offsetWidth - R.parent.minRightColWidth - PAD,
|
||||
),
|
||||
R.parent.minLeftColWidth,
|
||||
);
|
||||
setLeftColGridTemplate(R.parent, leftColWidth);
|
||||
}
|
||||
});
|
||||
|
|
@ -173,6 +182,13 @@
|
|||
R.tracking = false;
|
||||
|
||||
document.body.classList.remove('resizing');
|
||||
|
||||
// Fire a custom event at end of resizing.
|
||||
R.parent.dispatchEvent(
|
||||
new CustomEvent("resizeHandleResized", {
|
||||
bubbles: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -185,7 +201,7 @@
|
|||
for (const parent of parents) {
|
||||
afterResize(parent);
|
||||
}
|
||||
}, DEBOUNCE_TIME);
|
||||
}, DEBOUNCE_TIME_MS);
|
||||
});
|
||||
|
||||
setupResizeHandle = setup;
|
||||
|
|
|
|||
742
javascript/utils.js
Normal file
742
javascript/utils.js
Normal file
|
|
@ -0,0 +1,742 @@
|
|||
/** Collators used for sorting. */
|
||||
const INT_COLLATOR = new Intl.Collator([], {numeric: true});
|
||||
const STR_COLLATOR = new Intl.Collator("en", {numeric: true, sensitivity: "base"});
|
||||
|
||||
/** Helper functions for checking types and simplifying logging/error handling. */
|
||||
function isNumber(x) {
|
||||
return typeof x === "number" && isFinite(x);
|
||||
}
|
||||
function isNumberLogError(x) {
|
||||
if (isNumber(x)) {
|
||||
return true;
|
||||
}
|
||||
console.error(`expected number, got: ${typeof x}`);
|
||||
return false;
|
||||
}
|
||||
function isNumberThrowError(x) {
|
||||
if (isNumber(x)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`expected number, got: ${typeof x}`);
|
||||
}
|
||||
|
||||
function isString(x) {
|
||||
return typeof x === "string" || x instanceof String;
|
||||
}
|
||||
function isStringLogError(x) {
|
||||
if (isString(x)) {
|
||||
return true;
|
||||
}
|
||||
console.error(`expected string, got: ${typeof x}`);
|
||||
return false;
|
||||
}
|
||||
function isStringThrowError(x) {
|
||||
if (isString(x)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`expected string, got: ${typeof x}`);
|
||||
}
|
||||
|
||||
function isNull(x) {
|
||||
return x === null;
|
||||
}
|
||||
function isUndefined(x) {
|
||||
return typeof x === "undefined" || x === undefined;
|
||||
}
|
||||
// checks both null and undefined for simplicity sake.
|
||||
function isNullOrUndefined(x) {
|
||||
return isNull(x) || isUndefined(x);
|
||||
}
|
||||
function isNullOrUndefinedLogError(x) {
|
||||
if (isNullOrUndefined(x)) {
|
||||
console.error("Variable is null/undefined.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isNullOrUndefinedThrowError(x) {
|
||||
if (!isNullOrUndefined(x)) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Variable is null/undefined.");
|
||||
}
|
||||
|
||||
function isElement(x) {
|
||||
return x instanceof Element;
|
||||
}
|
||||
function isElementLogError(x) {
|
||||
if (isElement(x)) {
|
||||
return true;
|
||||
}
|
||||
console.error(`expected element type, got: ${typeof x}`);
|
||||
return false;
|
||||
}
|
||||
function isElementThrowError(x) {
|
||||
if (isElement(x)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`expected element type, got: ${typeof x}`);
|
||||
}
|
||||
|
||||
function isFunction(x) {
|
||||
return typeof x === "function";
|
||||
}
|
||||
function isFunctionLogError(x) {
|
||||
if (isFunction(x)) {
|
||||
return true;
|
||||
}
|
||||
console.error(`expected function type, got: ${typeof x}`);
|
||||
return false;
|
||||
}
|
||||
function isFunctionThrowError(x) {
|
||||
if (isFunction(x)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`expected function type, got: ${typeof x}`);
|
||||
}
|
||||
|
||||
function isObject(x) {
|
||||
return typeof x === "object" && !Array.isArray(x);
|
||||
}
|
||||
function isObjectLogError(x) {
|
||||
if (isObject(x)) {
|
||||
return true;
|
||||
}
|
||||
console.error(`expected object type, got: ${typeof x}`);
|
||||
return false;
|
||||
}
|
||||
function isObjectThrowError(x) {
|
||||
if (isObject(x)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`expected object type, got: ${typeof x}`);
|
||||
}
|
||||
|
||||
function keyExists(obj, k) {
|
||||
return isObject(obj) && isString(k) && k in obj;
|
||||
}
|
||||
function keyExistsLogError(obj, k) {
|
||||
if (keyExists(obj, k)) {
|
||||
return true;
|
||||
}
|
||||
console.error(`key does not exist in object: ${k}`);
|
||||
return false;
|
||||
}
|
||||
function keyExistsThrowError(obj, k) {
|
||||
if (keyExists(obj, k)) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`key does not exist in object: ${k}`);
|
||||
}
|
||||
|
||||
function getValue(obj, k) {
|
||||
/** Returns value of object for given key if it exists, otherwise returns null. */
|
||||
if (keyExists(obj, k)) {
|
||||
return obj[k];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getValueLogError(obj, k) {
|
||||
if (keyExistsLogError(obj, k)) {
|
||||
return obj[k];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function getValueThrowError(obj, k) {
|
||||
keyExistsThrowError(obj, k);
|
||||
return obj[k];
|
||||
}
|
||||
|
||||
function getElementByIdLogError(selector) {
|
||||
const elem = gradioApp().getElementById(selector);
|
||||
isElementLogError(elem);
|
||||
return elem;
|
||||
}
|
||||
function getElementByIdThrowError(selector) {
|
||||
const elem = gradioApp().getElementById(selector);
|
||||
isElementThrowError(elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
function querySelectorLogError(selector) {
|
||||
const elem = gradioApp().querySelector(selector);
|
||||
isElementLogError(elem);
|
||||
return elem;
|
||||
}
|
||||
function querySelectorThrowError(selector) {
|
||||
const elem = gradioApp().querySelector(selector);
|
||||
isElementThrowError(elem);
|
||||
return elem;
|
||||
}
|
||||
|
||||
const validateArrayType = (arr, type_check_fn) => {
|
||||
/** Validates that a variable is an array with members of a specified type.
|
||||
* `type_check_fn` must accept array elements as arguments and return whether
|
||||
* they match the expected type.
|
||||
* `arr` will be wrapped in an array if it is not already an array.
|
||||
*/
|
||||
isNullOrUndefinedThrowError(type_check_fn);
|
||||
if (isNullOrUndefined(arr)) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(arr) && type_check_fn(arr)) {
|
||||
return [arr];
|
||||
} else if (Array.isArray(arr) && arr.every((x) => type_check_fn(x))) {
|
||||
return arr;
|
||||
} else {
|
||||
throw new Error('Invalid array types:', arr);
|
||||
}
|
||||
};
|
||||
|
||||
/** Functions for getting dimensions of elements. */
|
||||
function getStyle(elem) {
|
||||
return window.getComputedStyle ? window.getComputedStyle(elem) : elem.currentStyle;
|
||||
}
|
||||
|
||||
function getComputedProperty(elem, prop) {
|
||||
return getStyle(elem)[prop];
|
||||
}
|
||||
|
||||
function getComputedPropertyDims(elem, prop) {
|
||||
/** Returns the top/left/bottom/right float dimensions of an element for the specified property. */
|
||||
const style = getStyle(elem);
|
||||
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`)),
|
||||
};
|
||||
}
|
||||
|
||||
function 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,
|
||||
};
|
||||
}
|
||||
|
||||
function 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,
|
||||
};
|
||||
}
|
||||
|
||||
function 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,
|
||||
};
|
||||
}
|
||||
|
||||
function 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,
|
||||
};
|
||||
}
|
||||
|
||||
/** Functions for asynchronous operations. */
|
||||
|
||||
function waitForElement(selector, timeout_ms) {
|
||||
/** Promise that waits for an element to exist in DOM. */
|
||||
return new Promise((resolve, reject) => {
|
||||
if (document.querySelector(selector)) {
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
if (document.querySelector(selector)) {
|
||||
observer.disconnect();
|
||||
return resolve(document.querySelector(selector));
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
||||
setTimeout(() => {
|
||||
observer.takeRecords();
|
||||
observer.disconnect();
|
||||
return reject(`timed out waiting for element: "${selector}"`);
|
||||
}, timeout_ms);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function waitForBool(o, timeout_ms) {
|
||||
/** Promise that waits for a boolean to be true.
|
||||
*
|
||||
* `o` must be an Object of the form:
|
||||
* { state: <bool value> }
|
||||
*
|
||||
* If timeout_ms is null/undefined or 0, waits forever.
|
||||
*
|
||||
* Resolves when (state === true)
|
||||
* Rejects when state is not True before timeout_ms.
|
||||
*/
|
||||
let wait_timer;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
||||
setTimeout(() => {
|
||||
clearTimeout(wait_timer);
|
||||
return reject("timed out waiting for bool");
|
||||
}, timeout_ms);
|
||||
}
|
||||
(function _waitForBool() {
|
||||
if (o.state) {
|
||||
return resolve();
|
||||
}
|
||||
wait_timer = setTimeout(_waitForBool, 100);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForKeyInObject(o, timeout_ms) {
|
||||
/** 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>,
|
||||
* }
|
||||
*
|
||||
* If timeout_ms is null/undefined or 0, waits forever.
|
||||
*
|
||||
* Resolves when (k in obj).
|
||||
* Rejects when k is not found in obj before timeout_ms.
|
||||
*/
|
||||
let wait_timer;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
||||
setTimeout(() => {
|
||||
clearTimeout(wait_timer);
|
||||
return reject(`timed out waiting for key: ${o.k}`);
|
||||
}, timeout_ms);
|
||||
}
|
||||
(function _waitForKeyInObject() {
|
||||
if (o.k in o.obj) {
|
||||
return resolve();
|
||||
}
|
||||
wait_timer = setTimeout(_waitForKeyInObject, 100);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
function waitForValueInObject(o, timeout_ms) {
|
||||
/** 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>
|
||||
* }
|
||||
*
|
||||
* If timeout_ms is null/undefined or 0, waits forever.
|
||||
*
|
||||
* Resolves when obj[k] == v
|
||||
*/
|
||||
let wait_timer;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isNumber(timeout_ms) && timeout_ms !== 0) {
|
||||
setTimeout(() => {
|
||||
clearTimeout(wait_timer);
|
||||
return reject(`timed out waiting for value: ${o.k}: ${o.v}`);
|
||||
}, timeout_ms);
|
||||
}
|
||||
waitForKeyInObject({k: o.k, obj: o.obj}, timeout_ms).then(() => {
|
||||
(function _waitForValueInObject() {
|
||||
|
||||
if (o.k in o.obj && o.obj[o.k] == o.v) {
|
||||
return resolve();
|
||||
}
|
||||
setTimeout(_waitForValueInObject, 100);
|
||||
})();
|
||||
}).catch((error) => {
|
||||
return reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Requests */
|
||||
|
||||
class FetchError extends Error {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch4xxError extends FetchError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
class Fetch5xxError extends FetchError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
class FetchRetryLimitError extends FetchError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
class FetchTimeoutError extends FetchError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
class FetchWithRetryAndBackoffTimeoutError extends FetchError {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithRetryAndBackoff(url, data, args = {}) {
|
||||
/** Wrapper around `fetch` with retries, backoff, and timeout.
|
||||
*
|
||||
* Uses a Decorrelated jitter backoff strategy.
|
||||
* https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
||||
*
|
||||
* Args:
|
||||
* url: Primary URL to fetch.
|
||||
* data: Data to append to the URL when making the request.
|
||||
* args:
|
||||
* method: The HTTP request method to use.
|
||||
* timeout_ms: Max allowed time before this function fails.
|
||||
* fetch_timeout_ms: Max allowed time for individual `fetch` calls.
|
||||
* min_delay_ms: Min time that a delay between requests can be.
|
||||
* max_delay_ms: Max time that a delay between reqeusts can be.
|
||||
* response_handler: A callback function that returns a promise.
|
||||
* This function is sent the response from the `fetch` call.
|
||||
* If not specified, all status codes >= 400 are handled as errors.
|
||||
* This is useful for handling requests whose responses from the server
|
||||
* are erronious but the HTTP status is 200.
|
||||
*/
|
||||
args.method = args.method || "GET";
|
||||
args.timeout_ms = args.timeout_ms || 30000;
|
||||
args.min_delay_ms = args.min_delay_ms || 100;
|
||||
args.max_delay_ms = args.max_delay_ms || 3000;
|
||||
args.fetch_timeout_ms = args.fetch_timeout_ms || 10000;
|
||||
// The default response handler function for `fetch` call responses.
|
||||
const response_handler = (response) => new Promise((resolve, reject) => {
|
||||
if (response.ok) {
|
||||
return response.json().then(json => {
|
||||
return resolve(json);
|
||||
});
|
||||
} else {
|
||||
if (response.status >= 400 && response.status < 500) {
|
||||
throw new Fetch4xxError("client error:", response);
|
||||
}
|
||||
if (response.status >= 500 && response.status < 600) {
|
||||
throw new Fetch5xxError("server error:", response);
|
||||
}
|
||||
return reject(response);
|
||||
}
|
||||
});
|
||||
args.response_handler = args.response_handler || response_handler;
|
||||
|
||||
const url_args = Object.entries(data).map(([k, v]) => {
|
||||
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
||||
}).join("&");
|
||||
url = `${url}?${url_args}`;
|
||||
|
||||
let controller;
|
||||
let retry = true;
|
||||
|
||||
const randrange = (min, max) => {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
};
|
||||
const get_jitter = (base, max, prev) => {
|
||||
return Math.min(max, randrange(base, prev * 3));
|
||||
};
|
||||
|
||||
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
// Timeout for individual `fetch` calls.
|
||||
const fetch_timeout = (ms, promise) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
return reject(new FetchTimeoutError("Fetch timed out."));
|
||||
}, ms);
|
||||
return promise.then(resolve, reject);
|
||||
});
|
||||
};
|
||||
|
||||
// Timeout for all retries.
|
||||
const run_timeout = (ms, promise) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
retry = false;
|
||||
controller.abort();
|
||||
return reject(new FetchWithRetryAndBackoffTimeoutError("Request timed out."));
|
||||
}, ms);
|
||||
return promise.then(resolve, reject);
|
||||
});
|
||||
};
|
||||
|
||||
const run = async(delay_ms) => {
|
||||
if (!retry) {
|
||||
// Retry is controlled externally via `run_timeout`. This function's promise
|
||||
// is also handled via that timeout so we can just return here.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
controller = new AbortController();
|
||||
const fetch_opts = {method: args.method, signal: controller.signal};
|
||||
const response = await fetch_timeout(args.fetch_timeout_ms, fetch(url, fetch_opts));
|
||||
return await args.response_handler(response);
|
||||
} catch (error) {
|
||||
controller.abort();
|
||||
// dont bother with anything else if told to not retry.
|
||||
if (!retry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Fetch4xxError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof Fetch5xxError) {
|
||||
throw error;
|
||||
}
|
||||
// Any other errors mean we need to retry the request.
|
||||
delay_ms = get_jitter(args.min_delay_ms, args.max_delay_ms, delay_ms);
|
||||
await delay(delay_ms);
|
||||
return await run(delay_ms);
|
||||
}
|
||||
};
|
||||
return await run_timeout(args.timeout_ms, run(args.min_delay_ms));
|
||||
}
|
||||
|
||||
function requestGet(url, data, handler, errorHandler) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
var args = Object.keys(data).map(function(k) {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]);
|
||||
}).join('&');
|
||||
xhr.open("GET", url + "?" + args, true);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var js = JSON.parse(xhr.responseText);
|
||||
handler(js);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
errorHandler();
|
||||
}
|
||||
} else {
|
||||
errorHandler();
|
||||
}
|
||||
}
|
||||
};
|
||||
var js = JSON.stringify(data);
|
||||
xhr.send(js);
|
||||
}
|
||||
|
||||
function requestGetPromise(url, data, timeout_ms) {
|
||||
/**Asynchronous `GET` request that returns a promise.
|
||||
*
|
||||
* The result will be of the format {status: int, response: JSON object}.
|
||||
* Thus, the xhr.responseText that we receive is expected to be a JSON string.
|
||||
* Acceptable status codes for successful requests are 200 <= status < 300.
|
||||
*/
|
||||
if (!isNumber(timeout_ms)) {
|
||||
timeout_ms = 1000;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const args = Object.entries(data).map(([k, v]) => {
|
||||
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
|
||||
}).join("&");
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
return resolve({status: xhr.status, response: JSON.parse(xhr.responseText)});
|
||||
} else {
|
||||
return reject({status: xhr.status, response: JSON.parse(xhr.responseText)});
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
return reject({status: xhr.status, response: JSON.parse(xhr.responseText)});
|
||||
};
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
return reject({status: 408, response: {detail: `Request timeout: ${url}`}});
|
||||
};
|
||||
|
||||
const payload = JSON.stringify(data);
|
||||
xhr.open("GET", `${url}?${args}`, true);
|
||||
xhr.timeout = timeout_ms;
|
||||
xhr.send(payload);
|
||||
});
|
||||
}
|
||||
|
||||
/** Misc helper functions. */
|
||||
|
||||
function clamp(x, min, max) {
|
||||
return Math.max(min, Math.min(x, max));
|
||||
}
|
||||
|
||||
function htmlStringToElement(s) {
|
||||
/** Converts an HTML string into an Element type. */
|
||||
let parser = new DOMParser();
|
||||
let tmp = parser.parseFromString(s, "text/html");
|
||||
return tmp.body.firstElementChild;
|
||||
}
|
||||
|
||||
function htmlStringToFragment(s) {
|
||||
/** Converts an HTML string into a DocumentFragment. */
|
||||
return document.createRange().createContextualFragment(s);
|
||||
}
|
||||
|
||||
function convertInnerHtmlToShadowDOM(elem) {
|
||||
/** Inplace conversion of innerHTML of an element into a Shadow DOM.
|
||||
*
|
||||
* If the innerHTML is not valid HTML then the innerHTML is left unchanged.
|
||||
*/
|
||||
const parsed_str = new DOMParser().parseFromString(elem.innerHTML, "text/html").documentElement.textContent;
|
||||
const parsed_elem = htmlStringToElement(parsed_str);
|
||||
if (!isNullOrUndefined(parsed_elem)) {
|
||||
elem.innerHTML = "";
|
||||
const shadow = elem.attachShadow({mode: "open"});
|
||||
shadow.appendChild(parsed_elem);
|
||||
}
|
||||
}
|
||||
|
||||
function convertElementShadowDOM(elem, selector) {
|
||||
/** Inplace conversion of Shadow DOM of all children matching the passed selector.
|
||||
*
|
||||
* `selector` defaults to [data-parse-as-shadow-dom] if not a valid string.
|
||||
*
|
||||
* NOTE: Nested Shadow DOMs are untested but will likely not work.
|
||||
*/
|
||||
if (!isString(selector)) {
|
||||
selector = "[data-parse-as-shadow-dom]";
|
||||
}
|
||||
|
||||
let children = Array.from(elem.querySelectorAll(selector));
|
||||
children = children.filter(x => x.innerHTML !== "");
|
||||
for (const child of children) {
|
||||
convertInnerHtmlToShadowDOM(child);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCss(key, css, enable) {
|
||||
var style = document.getElementById(key);
|
||||
if (enable && !style) {
|
||||
style = document.createElement('style');
|
||||
style.id = key;
|
||||
style.type = 'text/css';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
if (style && !enable) {
|
||||
document.head.removeChild(style);
|
||||
}
|
||||
if (style) {
|
||||
style.innerHTML == '';
|
||||
style.appendChild(document.createTextNode(css));
|
||||
}
|
||||
}
|
||||
|
||||
function copyToClipboard(s) {
|
||||
/** Copies the passed string to the clipboard. */
|
||||
isStringThrowError(s);
|
||||
navigator.clipboard.writeText(s);
|
||||
}
|
||||
|
||||
function attrPromise({elem, attr, timeout_ms} = {}) {
|
||||
timeout_ms = timeout_ms || 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
let res = false;
|
||||
const observer_config = {attributes: true, attributeOldValue: true};
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
if (isString(attr) && mutation.attributeName === attr) {
|
||||
res = true;
|
||||
observer.disconnect();
|
||||
resolve(elem, elem.getAttribute(attr));
|
||||
}
|
||||
if (!isString(attr)) {
|
||||
res = true;
|
||||
observer.disconnect();
|
||||
resolve(elem);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (timeout_ms > 0) {
|
||||
setTimeout(() => {
|
||||
if (!res) {
|
||||
reject(elem);
|
||||
}
|
||||
}, timeout_ms);
|
||||
}
|
||||
|
||||
if (isString(attr)) {
|
||||
observer_config.attributeFilter = [attr];
|
||||
}
|
||||
observer.observe(elem, observer_config);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForVisible(elem, callback) {
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.intersectionRatio > 0) {
|
||||
callback(elem);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}).observe(elem);
|
||||
if (!callback) return new Promise(resolve => callback = resolve);
|
||||
}
|
||||
|
||||
function cssRelativeUnitToPx(css_value, target) {
|
||||
// https://stackoverflow.com/a/66569574
|
||||
// doesnt work on `%` unit.
|
||||
target = target || document.body;
|
||||
const units = {
|
||||
px: x => x, // no conversion needed here
|
||||
rem: x => x * parseFloat(getComputedStyle(document.documentElement).fontSize),
|
||||
em: x => x * parseFloat(getComputedStyle(target).fontSize),
|
||||
vw: x => x / 100 * window.innerWidth,
|
||||
vh: x => x / 100 * window.innerHeight,
|
||||
};
|
||||
|
||||
const re = new RegExp(`^([-+]?(?:\\d+(?:\\.\\d+)?))(${Object.keys(units).join('|')})$`, 'i');
|
||||
const matches = css_value.toString().trim().match(re);
|
||||
if (matches) {
|
||||
const value = Number(matches[1]);
|
||||
const unit = matches[2].toLocaleLowerCase();
|
||||
if (unit in units) {
|
||||
return units[unit](value);
|
||||
}
|
||||
}
|
||||
return css_value;
|
||||
}
|
||||
|
|
@ -273,9 +273,12 @@ options_templates.update(options_section(('interrogate', "Interrogate"), {
|
|||
}))
|
||||
|
||||
options_templates.update(options_section(('extra_networks', "Extra Networks", "sd"), {
|
||||
"extra_networks_show_hidden_directories": OptionInfo(True, "Show hidden directories").info("directory is hidden if its name starts with \".\"."),
|
||||
"extra_networks_dir_button_function": OptionInfo(False, "Add a '/' to the beginning of directory buttons").info("Buttons will display the contents of the selected directory without acting as a search filter."),
|
||||
"extra_networks_hidden_models": OptionInfo("When searched", "Show cards for models in hidden directories", gr.Radio, {"choices": ["Always", "When searched", "Never"]}).info('"When searched" option will only show the item when the search string has 4 characters or more'),
|
||||
"extra_networks_show_hidden_directories_buttons": OptionInfo(False, "Show buttons for hidden directories in the directory and tree views.").info("a directory is hidden if its name starts with a period (\".\").").needs_reload_ui(),
|
||||
"extra_networks_show_hidden_models_cards": OptionInfo("Never", "Show cards for models in hidden directories", gr.Radio, {"choices": ["Always", "When searched", "Never"]}).info("\"When searched\" will only show cards when the search string has 4 characters or more and the search string matches either the model name or the hidden directory name (or any of its subdirectories).").needs_reload_ui(),
|
||||
"extra_networks_show_hidden_models_in_tree_view": OptionInfo(False, "Show entries for models inside hidden directories in the tree view.").info("This option only applies if the \"Show buttons for hidden directories\" option is enabled.").needs_reload_ui(),
|
||||
"extra_networks_tree_view_expand_depth_default": OptionInfo(0, "Expand all directories in the tree view up to this depth by default.").info("0 collapses all, -1 expands all.").needs_reload_ui(),
|
||||
"extra_networks_directory_filter_click_behavior": OptionInfo("Click", "Filter directory recursively left mouse button action.", gr.Radio, {"choices": ["Click", "Long Press"]}).info("Sets the default left mouse button action required to filter a directory recursively (show children in all subdirectories) vs filtering to only show direct children of the selected directory."),
|
||||
"extra_networks_card_details_click_behavior": OptionInfo("Click", "Show card details left mouse button action.", gr.Radio, {"choices": ["Click", "Long Press"]}).info("Sets the default left mouse button action when clicking a card."),
|
||||
"extra_networks_default_multiplier": OptionInfo(1.0, "Default multiplier for extra networks", gr.Slider, {"minimum": 0.0, "maximum": 2.0, "step": 0.01}),
|
||||
"extra_networks_card_width": OptionInfo(0, "Card width for Extra Networks").info("in pixels"),
|
||||
"extra_networks_card_height": OptionInfo(0, "Card height for Extra Networks").info("in pixels"),
|
||||
|
|
@ -284,14 +287,21 @@ options_templates.update(options_section(('extra_networks', "Extra Networks", "s
|
|||
"extra_networks_card_description_is_html": OptionInfo(False, "Treat card description as HTML"),
|
||||
"extra_networks_card_order_field": OptionInfo("Path", "Default order field for Extra Networks cards", gr.Dropdown, {"choices": ['Path', 'Name', 'Date Created', 'Date Modified']}).needs_reload_ui(),
|
||||
"extra_networks_card_order": OptionInfo("Ascending", "Default order for Extra Networks cards", gr.Dropdown, {"choices": ['Ascending', 'Descending']}).needs_reload_ui(),
|
||||
"extra_networks_tree_view_style": OptionInfo("Dirs", "Extra Networks directory view style", gr.Radio, {"choices": ["Tree", "Dirs"]}).needs_reload_ui(),
|
||||
"extra_networks_tree_view_default_enabled": OptionInfo(True, "Show the Extra Networks directory view by default").needs_reload_ui(),
|
||||
"extra_networks_tree_view_default_width": OptionInfo(180, "Default width for the Extra Networks directory tree view", gr.Number).needs_reload_ui(),
|
||||
"extra_networks_dirs_view_default_enabled": OptionInfo(True, "Show the Extra Networks directory view by default.").needs_reload_ui(),
|
||||
"extra_networks_tree_view_default_enabled": OptionInfo(False, "Show the Extra Networks tree view by default").needs_reload_ui(),
|
||||
"extra_networks_card_view_default_enabled": OptionInfo(True, "Show the Extra Networks card view by default").needs_reload_ui(),
|
||||
"extra_networks_dets_view_default_enabled": OptionInfo(False, "Show the Extra Networks card details view by default").needs_reload_ui(),
|
||||
"extra_networks_tree_view_show_files": OptionInfo(True, "Show files in tree view.").info("Disabling this option will remove file entries from the tree view and only show directories.").needs_reload_ui(),
|
||||
"extra_networks_dirs_view_default_height": OptionInfo(90, "Default height for the Directory Buttons view", gr.Number).info("in pixels").needs_reload_ui(),
|
||||
"extra_networks_tree_view_default_width": OptionInfo(180, "Default width for the Extra Networks directory tree view", gr.Number).info("in pixels").needs_reload_ui(),
|
||||
"extra_networks_dets_view_default_width": OptionInfo(180, "Default width for the Extra Networks card details view", gr.Number).info("in pixels").needs_reload_ui(),
|
||||
"extra_networks_add_text_separator": OptionInfo(" ", "Extra networks separator").info("extra text to add before <...> when adding extra network to prompt"),
|
||||
"ui_extra_networks_tab_reorder": OptionInfo("", "Extra networks tab order").needs_reload_ui(),
|
||||
"textual_inversion_print_at_load": OptionInfo(False, "Print a list of Textual Inversion embeddings when loading model"),
|
||||
"textual_inversion_add_hashes_to_infotext": OptionInfo(True, "Add Textual Inversion hashes to infotext"),
|
||||
"sd_hypernetwork": OptionInfo("None", "Add hypernetwork to prompt", gr.Dropdown, lambda: {"choices": ["None", *shared.hypernetworks]}, refresh=shared_items.reload_hypernetworks),
|
||||
"extra_networks_long_press_time_ms": OptionInfo(800, "Hold time required to register a long click").info("in milliseconds").info("default 800"),
|
||||
"extra_networks_dbl_press_time_ms": OptionInfo(500, "Time between clicks to register a double click").info("in milliseconds").info("default 500"),
|
||||
"textual_inversion_image_embedding_data_cache": OptionInfo(False, 'Cache the data of image embeddings').info('potentially increase TI load time at the cost some disk space'),
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ def setup_dialog(button_show, dialog, *, button_close=None):
|
|||
fn=lambda: gr.update(visible=True),
|
||||
inputs=[],
|
||||
outputs=[dialog],
|
||||
).then(fn=None, _js="function(){ popupId('" + dialog.elem_id + "'); }")
|
||||
).then(fn=None, _js=f"function(){{popupId('{dialog.elem_id}');}}")
|
||||
|
||||
if button_close:
|
||||
button_close.click(fn=None, _js="closePopup")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
import html
|
||||
import os
|
||||
|
||||
from modules import shared, ui_extra_networks, sd_models
|
||||
|
|
@ -13,6 +12,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage):
|
|||
|
||||
def refresh(self):
|
||||
shared.refresh_checkpoints()
|
||||
super().refresh()
|
||||
|
||||
def create_item(self, name, index=None, enable_filter=True):
|
||||
checkpoint: sd_models.CheckpointInfo = sd_models.checkpoint_aliases.get(name)
|
||||
|
|
@ -30,7 +30,6 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage):
|
|||
"preview": self.find_preview(path),
|
||||
"description": self.find_description(path),
|
||||
"search_terms": search_terms,
|
||||
"onclick": html.escape(f"return selectCheckpoint({ui_extra_networks.quote_js(name)})"),
|
||||
"local_preview": f"{path}.{shared.opts.samples_format}",
|
||||
"metadata": checkpoint.metadata,
|
||||
"sort_keys": {'default': index, **self.get_sort_keys(checkpoint.filename)},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage):
|
|||
|
||||
def refresh(self):
|
||||
shared.reload_hypernetworks()
|
||||
super().refresh()
|
||||
|
||||
def create_item(self, name, index=None, enable_filter=True):
|
||||
full_path = shared.hypernetworks.get(name)
|
||||
|
|
@ -30,7 +31,7 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage):
|
|||
"preview": self.find_preview(path),
|
||||
"description": self.find_description(path),
|
||||
"search_terms": search_terms,
|
||||
"prompt": quote_js(f"<hypernet:{name}:") + " + opts.extra_networks_default_multiplier + " + quote_js(">"),
|
||||
"prompt": quote_js(f"<hypernet:{name}:{shared.opts.extra_networks_default_multiplier}>"),
|
||||
"local_preview": f"{path}.preview.{shared.opts.samples_format}",
|
||||
"sort_keys": {'default': index, **self.get_sort_keys(path + ext)},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage):
|
|||
|
||||
def refresh(self):
|
||||
sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings(force_reload=True)
|
||||
super().refresh()
|
||||
|
||||
def create_item(self, name, index=None, enable_filter=True):
|
||||
embedding = sd_hijack.model_hijack.embedding_db.word_embeddings.get(name)
|
||||
|
|
|
|||
|
|
@ -148,7 +148,18 @@ class UserMetadataEditor:
|
|||
def setup_save_handler(self, button, func, components):
|
||||
button\
|
||||
.click(fn=func, inputs=[self.edit_name_input, *components], outputs=[])\
|
||||
.then(fn=None, _js="function(name){closePopup(); extraNetworksRefreshSingleCard(" + json.dumps(self.page.name) + "," + json.dumps(self.tabname) + ", name);}", inputs=[self.edit_name_input], outputs=[])
|
||||
.then(
|
||||
fn=None,
|
||||
_js=(
|
||||
"function(name){"
|
||||
"closePopup(); "
|
||||
"extraNetworksRefreshSingleCard("
|
||||
f"'{self.tabname}', '{self.page.extra_networks_tabname}', name"
|
||||
");}"
|
||||
),
|
||||
inputs=[self.edit_name_input],
|
||||
outputs=[],
|
||||
)
|
||||
|
||||
def create_editor(self):
|
||||
self.create_default_editor_elems()
|
||||
|
|
@ -199,7 +210,12 @@ class UserMetadataEditor:
|
|||
outputs=[self.html_preview, self.html_status]
|
||||
).then(
|
||||
fn=None,
|
||||
_js="function(name){extraNetworksRefreshSingleCard(" + json.dumps(self.page.name) + "," + json.dumps(self.tabname) + ", name);}",
|
||||
_js=(
|
||||
"function(name){"
|
||||
"extraNetworksRefreshSingleCard("
|
||||
f"'{self.tabname}', '{self.page.extra_networks_tabname}', name"
|
||||
");}"
|
||||
),
|
||||
inputs=[self.edit_name_input],
|
||||
outputs=[]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -120,4 +120,8 @@ class UiPromptStyles:
|
|||
inputs=[self.main_ui_prompt, self.main_ui_negative_prompt, self.dropdown],
|
||||
outputs=[self.main_ui_prompt, self.main_ui_negative_prompt, self.dropdown],
|
||||
show_progress=False,
|
||||
).then(fn=None, _js="function(){update_"+self.tabname+"_tokens(); closePopup();}", show_progress=False)
|
||||
).then(
|
||||
fn=None,
|
||||
_js=f"function(){{update_{self.tabname}_tokens(); closePopup();}}",
|
||||
show_progress=False,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue