From 4a2c80644871e9be824f3a834a72e69f99dcf81b Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Thu, 23 May 2024 17:11:49 -0400 Subject: [PATCH] Fix bugs with resize grid. Add documentation. Add example code. --- html/extra-networks-pane.html | 14 +- javascript/extraNetworks.js | 25 +- javascript/resizeGrid.js | 1233 +++++++++++++++-------------- javascript/resizeGridExample.html | 178 +++++ javascript/utils.js | 19 + modules/ui_extra_networks.py | 30 +- style.css | 7 +- 7 files changed, 865 insertions(+), 641 deletions(-) create mode 100644 javascript/resizeGridExample.html diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index 115673022..f518f9426 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -163,7 +163,7 @@
-
+
{dirs_html}
@@ -171,8 +171,8 @@
-
+
@@ -186,8 +186,8 @@
-
+
@@ -202,8 +202,8 @@
-
+
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index bee9cb3c4..225930336 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -10,6 +10,7 @@ fetchWithRetryAndBackoff, isElementLogError, isNumber, + getComputedBorderDims, waitForKeyInObject, isNullOrUndefined, debounce, @@ -370,10 +371,10 @@ class ExtraNetworksTab { const div_tree = this.container_elem.querySelector(".extra-network-content--tree-view"); const div_card = this.container_elem.querySelector(".extra-network-content--card-view"); const div_dets = this.container_elem.querySelector(".extra-network-content--dets-view"); - this.resize_grid.toggleElem(div_dirs, this.dirs_view_en); - this.resize_grid.toggleElem(div_tree, this.tree_view_en); - this.resize_grid.toggleElem(div_card, this.card_view_en); - this.resize_grid.toggleElem(div_dets, this.dets_view_en); + this.resize_grid.toggle({elem: div_dirs, override: this.dirs_view_en}); + this.resize_grid.toggle({elem: div_tree, override: this.tree_view_en}); + this.resize_grid.toggle({elem: div_card, override: this.card_view_en}); + this.resize_grid.toggle({elem: div_dets, override: this.dets_view_en}); await Promise.all([this.setupTreeList(), this.setupCardList()]); this.tree_list.enable(this.tree_view_en); @@ -438,10 +439,10 @@ class ExtraNetworksTab { const div_tree = this.container_elem.querySelector(".extra-network-content--tree-view"); const div_card = this.container_elem.querySelector(".extra-network-content--card-view"); const div_dets = this.container_elem.querySelector(".extra-network-content--dets-view"); - this.resize_grid.toggleElem(div_dirs, this.dirs_view_en); - this.resize_grid.toggleElem(div_tree, this.tree_view_en); - this.resize_grid.toggleElem(div_card, this.card_view_en); - this.resize_grid.toggleElem(div_dets, this.dets_view_en); + this.resize_grid.toggle({elem: div_dirs, override: this.dirs_view_en}); + this.resize_grid.toggle({elem: div_tree, override: this.tree_view_en}); + this.resize_grid.toggle({elem: div_card, override: this.card_view_en}); + this.resize_grid.toggle({elem: div_dets, override: this.dets_view_en}); this.tree_list.enable(this.tree_view_en); this.card_list.enable(this.card_view_en); @@ -1218,7 +1219,7 @@ async function extraNetworksControlTreeViewOnClick(event) { tab.tree_view_en = "selected" in btn.dataset; const div_tree = tab.container_elem.querySelector(".extra-network-content--tree-view"); - tab.resize_grid.toggleElem(div_tree, tab.tree_view_en); + tab.resize_grid.toggle({elem: div_tree, override: tab.tree_view_en}); tab.tree_list.enable(tab.tree_view_en); // If the tree list hasn't loaded yet, we need to force it to load. @@ -1243,7 +1244,7 @@ function extraNetworksControlDirsViewOnClick(event) { tab.dirs_view_en = "selected" in btn.dataset; const div_dirs = tab.container_elem.querySelector(".extra-network-content--dirs-view"); - tab.resize_grid.toggleElem(div_dirs, tab.dirs_view_en); + tab.resize_grid.toggle({elem: div_dirs, override: tab.dirs_view_en}); tab.applyListButtonStates(); } @@ -1260,7 +1261,7 @@ async function extraNetworksControlCardViewOnClick(event) { tab.card_view_en = "selected" in btn.dataset; const div_card = tab.container_elem.querySelector(".extra-network-content--card-view"); - tab.resize_grid.toggleElem(div_card, tab.card_view_en); + tab.resize_grid.toggle({elem: div_card, override: tab.card_view_en}); tab.card_list.enable(tab.card_view_en); // If the tree list hasn't loaded yet, we need to force it to load. @@ -1283,7 +1284,7 @@ function extraNetworksControlDetsViewOnClick(event) { tab.dets_view_en = "selected" in btn.dataset; const div_dets = tab.container_elem.querySelector(".extra-network-content--dets-view"); - tab.resize_grid.toggleElem(div_dets, tab.dets_view_en); + tab.resize_grid.toggle({elem: div_dets, override: tab.dets_view_en}); } function extraNetworksControlRefreshOnClick(event) { diff --git a/javascript/resizeGrid.js b/javascript/resizeGrid.js index 3b614e884..cbbc8ccfe 100644 --- a/javascript/resizeGrid.js +++ b/javascript/resizeGrid.js @@ -1,4 +1,39 @@ -/** @format */ +/** resizeGrid.js + * + * This allows generation of a grid of items which are separated by resizable handles. + * + * Generation of this grid is inferred from HTML. + * An example can be found in `javascript/resizeGridExample.html`. + * This example will generate a 2x2 grid with 2 rows and 2 cells per row. + * The top right and bottom left cells should fill the majority of their rows. + * Play around with the buttons to show/hide and drag to resize the grid. + * + * USAGE: + * Current limitations require that every row and column contain AT LEAST ONE + * child with flexGrow=1. CSS shortcuts are allowed (i.e. flex: 1 0 0px). + * + * You CANNOT have rows and columns at the same level. This will throw an error. + * + * flexBasis MUST be set for every item that does not have flexGrow=1. flexBasis can be + * set using `px` or any of the following relative units: [vh, vw, rem, em]. These + * units are limited by `utils.js::cssRelativeUnitToPx()`. + * + * The value set in flexGrow determines an item's size along its respective axis. + * So if you have a `resize-grid--col` item, then `flexBasis` will determine the item's + * height. `resize-grid--cell` items infer their axis from their parent element. + * + * You can also set a `data-min-size` attribute on every row/col/cell. If this attribute + * is not set, then the value set in flexBasis will be the minimum size of the item. + * If you are setting the flexBasis to 0, you MUST specify data-min-size="0px" as well + * otherwise the item's min_size will be set to its rendered size on build. + * + * If you do not set any `resize-grid--row` or `resize-grid--col` elements, but only + * set `resize-grid--cell` elements, then the grid will automatically wrap the cells + * in a single row. + * + * You can have a grid that only contains one row/col as long as that row/col contains + * at least one item. +*/ // Prevent eslint errors on functions defined in other files. /*global @@ -6,8 +41,12 @@ isNullOrUndefinedLogError, isNullOrUndefined, isString, + isStringThrowError, + isObject, + isFunction, cssRelativeUnitToPx, isNumber, + isNumberThrowError, isElementThrowError, isElement, */ @@ -18,7 +57,7 @@ const MOVE_TIME_DELAY_MS = 15; // Prevents handling element resize events too quickly. Lower values increase // computational load and may lead to lag when resizing. -const RESIZE_DEBOUNCE_TIME_MS = 100; +const RESIZE_DEBOUNCE_TIME_MS = 50; // The timeframe in which a second pointerup event must be fired to be treated // as a double click. const DBLCLICK_TIME_MS = 500; @@ -37,6 +76,7 @@ const _gen_id_string = () => { }; const _get_unique_id = () => { + /** Generates an ID string that does not exist in the `resize_grids` keys. */ let id = _gen_id_string(); while (id in Object.keys(resize_grids)) { id = _gen_id_string(); @@ -44,29 +84,15 @@ const _get_unique_id = () => { return id; }; -const _parse_array_type = (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. - */ - 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); - } -}; - const _axis_to_int = (axis) => { /** Converts an axis to a standardized axis integer. + * Args: + * axis: Integer or string to be parsed. * Returns: * "x" or 0: 0 * "y" or 1: 1 + * Throws: + * Error if the `axis` input is invalid. */ if (axis === 0 || axis === 'x') { return 0; @@ -78,26 +104,24 @@ const _axis_to_int = (axis) => { }; class ResizeGridHandle { - /** Class defining the clickable "handle" between two grid items. */ + /** The clickable "handle" between two ResizeGridItem instances. */ visible = true; id = null; // unique identifier for this instance. pad_px = PAD_PX; - constructor({id, parent, axis, class_list} = {}) { + constructor({id, parent, axis} = {}) { this.id = isNullOrUndefined(id) ? _gen_id_string() : id; this.parent = parent; this.elem = document.createElement('div'); this.elem.id = id; - this.elem.classList.add('resize-grid--handle'); - _parse_array_type(class_list, isString).forEach((class_name) => { - this.elem.classList.add(class_name); - }); - this.axis = _axis_to_int(axis); - + isNumberThrowError(this.axis); + this.elem.classList.add('resize-grid--handle'); if (this.axis === 0) { + this.elem.classList.add("resize-grid--row-handle"); this.elem.style.minHeight = this.pad_px + 'px'; this.elem.style.maxHeight = this.pad_px + 'px'; } else if (this.axis === 1) { + this.elem.classList.add("resize-grid--col-handle"); this.elem.style.minWidth = this.pad_px + 'px'; this.elem.style.maxWidth = this.pad_px + 'px'; } @@ -119,10 +143,37 @@ class ResizeGridHandle { } class ResizeGridItem { - /** Class defining the cells in a grid. These can be rows or columns. */ + /** Grid elements. These can be axes or individual cells. + * + * Attributes: + * id (str): + * A unique identifier for this instance and its element. + * parent (ResizeGridAxis,ResizeGrid): + * The container class for this item. Cannot be a base ResizeGridItem. + * elem (Element): + * The DOM element representing this item. + * callbacks (Object): + * Object specifying callbacks for various operations in this class. + * axis (int): + * The axis along which this item lies. 0: row, 1: col. + * This value is inferred from the passed element's class list. + * is_flex_grow (bool): + * Whether this item should auto expand along its axis. + * The actual elem.style.flexGrow may change independent of this variable + * but this variable is used to determine the default flexGrow. + * is_flex_shrink (bool): + * Whether this item should auto shrink along its axis. + * Same behavior as `is_flex_grow`. + * min_size (int): + * The minimum size of the element. + * base_size (int): + * The default size of the element. + * original_css_text (str): + * The elem.style.cssText string that the element had on initialization. + * Used during destruction of this instance to reset the element. + */ handle = null; visible = true; - pad_px = PAD_PX; callbacks = { /** Allows user to modify the `size_px` value passed to setSize(). * Default callback just returns null. (no op). @@ -133,13 +184,27 @@ class ResizeGridItem { * int: The overridden setSize `size_px` paremeter. If null/undefined, * then `size_px` does not get modified. */ - set_size_override: (item, size_px) => { return; }, + set_size_override: (item, size_px) => { + return; + }, }; constructor({id, parent, elem, axis, callbacks} = {}) { - this.id = isNullOrUndefined(id) ? _gen_id_string() : id; - this.parent = parent; // the parent class instance + if (elem.id) { + this.id = elem.id; + } else { + this.id = isNullOrUndefined(id) ? _gen_id_string() : id; + } + this.parent = parent; this.elem = elem; - this.axis = _axis_to_int(axis); + if (!isNullOrUndefined(axis)) { + this.axis = _axis_to_int(axis); + } else if (elem.classList.contains("resize-grid--row")) { + this.axis = 0; + } else if (elem.classList.contains("resize-grid--col")) { + this.axis = 1; + } else { + throw new Error("Unable to infer axis from element:", elem); + } // Parse user specified callback overrides. if (isObject(callbacks)) { @@ -149,48 +214,65 @@ class ResizeGridItem { } this.is_flex_grow = Boolean(parseInt(this.elem.style.flexGrow)); - this.default_is_flex_grow = this.is_flex_grow; + this.is_flex_shrink = Boolean(parseInt(this.elem.style.flexShrink)); let flex_basis = parseInt(cssRelativeUnitToPx(this.elem.style.flexBasis)); - // If user specifies data-min-size, then the flexBasis is just used to set - // the initial size. - if ('minSize' in this.elem.dataset) { - this.min_size = parseInt(cssRelativeUnitToPx(this.elem.dataset.minSize)); - if (isNumber(flex_basis)) { - this.base_size = flex_basis; - } else { - this.base_size = this.min_size; - } - } else if (isNumber(flex_basis)) { - this.min_size = flex_basis; + if (isNumber(flex_basis) && flex_basis > 0) { this.base_size = flex_basis; } else { - this.min_size = 0; - this.base_size = 0; + const dims = this.elem.getBoundingClientRect(); + this.base_size = parseInt(this.axis === 0 ? dims.height : dims.width); } - const dims = this.elem.getBoundingClientRect(); - this.base_size = - this.axis === 0 ? parseInt(dims.height) : parseInt(dims.width); + this.min_size = this.base_size; + + // If data-min-size is set, use that for the min_size instead. + if ("minSize" in this.elem.dataset) { + this.min_size = parseInt(cssRelativeUnitToPx(this.elem.dataset.minSize)); + } + this.elem.dataset.id = this.id; this.original_css_text = this.elem.style.cssText; } - render({force_flex_grow, reset} = {}) { - /** Sets the element's flex styles. */ + render({force_flex_grow, force_flex_shrink, reset} = {}) { + /** Sets the element's flex styles. + * + * If no arguments are passed, then flexGrow is reset to default and the + * flexBasis is set to the current calculated size of the element + * (clamped to min_size). + * + * Args: + * force_flex_grow (bool): + * Sets flexGrow=1 and does nothing else. + * force_flex_shrink (bool): + * Sets flexShrink=1 and does nothing else. + * reset (bool): + * Sets flexGrow and flexBasis to the instance defaults. + */ + if (!this.visible) { + return; + } + force_flex_grow = force_flex_grow === true; + force_flex_shrink = force_flex_shrink === true; reset = reset === true; - this.elem.style.flexShrink = 0; - if (reset) { + const vis_items = this.parent.items.filter(x => x.visible); + if (vis_items.length === 1) { + force_flex_grow = true; + force_flex_shrink = true; + } + + if (force_flex_grow || force_flex_shrink) { + this.elem.style.flexGrow = force_flex_grow ? 1 : parseInt(this.elem.style.flexGrow); + this.elem.style.flexShrink = force_flex_shrink ? 1 : parseInt(this.elem.style.flexShrink); + } else if (reset) { this.elem.style.flexGrow = Number(this.is_flex_grow); - this.elem.style.flexBasis = parseInt(this.base_size) + 'px'; - } else if (force_flex_grow) { - this.elem.style.flexGrow = 1; + this.elem.style.flexShrink = Number(this.is_flex_shrink); + this.elem.style.flexBasis = this.base_size + 'px'; } else { this.elem.style.flexGrow = Number(this.is_flex_grow); - if (!this.is_flex_grow) { - this.elem.style.flexBasis = - parseInt(Math.max(this.min_size, this.getSize())) + 'px'; - } + this.elem.style.flexShrink = Number(this.is_flex_shrink); + this.elem.style.flexBasis = Math.max(this.min_size, this.getSize()) + 'px'; } } @@ -207,26 +289,60 @@ class ResizeGridItem { } shrink(px, {limit_to_base} = {}) { - /** Shrink size along axis by specified pixels. Returns remainder. */ + /** Shrink size along axis by specified pixels. Returns remainder. + * + * Args: + * px (int): + * The number of pixels to shrink by. + * If -1, then the item is shrunk to the `base_size` or `min_size` + * depending on the value of `limit_to_base`. + * limit_to_base (bool): + * Whether to use the `base_size` as the minimum size after shrinking. + * If not specified or false, then `min_size` is used. + * + * Returns: + * int: The number of pixels remaining that could not be shrunk. + */ + if (px <= 0) { + return 0; + } + limit_to_base = limit_to_base === true; const target_size = limit_to_base ? this.base_size : this.min_size; const curr_size = this.getSize(); if (px === -1) { - // shrink to min_size + // Shrink to target regardless of the current size. this.setSize(target_size); return 0; + } else if (curr_size <= target_size) { + // This can happen if using base_size instead of min_size and the item is + // manually resized smaller than base_size. Cannot shrink, make no changes. + return px; } else if (curr_size - target_size < px) { + // Can shrink but not to the requested amount. Return remainder. this.setSize(target_size); return px - (curr_size - target_size); } else { + // Can shrink the full requested amount. this.setSize(curr_size - px); return 0; } } grow(px, {only_if_flex} = {}) { - /** Grows along axis and returns the amount grown in pixels. */ + /** Grows along axis and returns the amount grown in pixels. + * + * Args: + * px (int): + * The number of pixels to grow by. + * If -1, then the item grows to fill its container. + * only_if_flex (bool): + * If true, then grow is only performed if is_flex_grow=true. + * + * Returns: + * int: The number of pixels that the item grew by. + */ only_if_flex = only_if_flex === true; if (only_if_flex && !this.is_flex_grow) { return 0; @@ -247,19 +363,36 @@ class ResizeGridItem { } setSize(size_px) { + /** Sets the flexBasis value for the item. + * + * Prior to setting flexBasis, the `set_size_override` callback is called. + * If this callback returns a valid number, then we use that value instead + * of `size_px` for the new size. + * + * Args: + * size_px (int): The new size (in pixels). + * + * Returns: + * int: The size that we just set. + */ const new_size_px = this.callbacks.set_size_override(this, size_px); if (isNumber(new_size_px)) { size_px = new_size_px; } this.elem.style.flexBasis = parseInt(size_px) + 'px'; - //this.render(); return size_px; } getSize() { - // If this item is visible, then we can use the computed dimensions. - // Otherwise we are forced to use the flexBasis inline style. + /** Returns the current size of this item. + * + * Returns: + * int: If this item is visible in the DOM, then we return the computed + * dimensions of the element. Otherwise we return the element's + * inline style flexBasis value. + */ if (this.visible) { + const dims = this.elem.getBoundingClientRect(); return this.axis === 0 ? parseInt(dims.height) : parseInt(dims.width); } else { @@ -267,13 +400,12 @@ class ResizeGridItem { } } - genHandle(class_list) { - /** Generates a ResizeGridHandle after this item based on the axis. */ + genHandle() { + /** Generates a ResizeGridHandle for this item and returns the new handle. */ this.handle = new ResizeGridHandle({ id: `${this.id}_handle`, parent: this.parent, axis: this.axis, - class_list: class_list, }); if (isElement(this.elem.nextElementSibling)) { this.elem.parentElement.insertBefore( @@ -283,13 +415,16 @@ class ResizeGridItem { } else { this.elem.parentElement.appendChild(this.handle.elem); } + return this.handle; } show() { /** Shows this item and its ResizeGridHandle. */ this.elem.classList.remove('hidden'); - // Only show the handle if there is another ResizeGridItem after this one. - if (!isNullOrUndefined(this.handle.elem.nextSibling)) { + // Only show the handle if it isnt the last visible item along its axis. + const siblings = this.parent.items; + const sibling = siblings.slice(siblings.indexOf(this) + 1).find(x => x.visible); + if (sibling instanceof ResizeGridItem) { this.handle.show(); } this.visible = true; @@ -303,528 +438,391 @@ class ResizeGridItem { } } -class ResizeGridContainer { - /** Class defining a collection of ResizeGridItem and ResizeGridHandle instances. */ - constructor({id, parent, elem, callbacks} = {}) { - this.id = isNullOrUndefined(id) ? _gen_id_string() : id; - this.parent = parent; - this.elem = elem; - this.callbacks = callbacks; - this.original_css_text = this.elem.style.cssText; - - this.grid = []; - this.rows = []; - this.id_map = {}; - this.added_outer_row = false; +class ResizeGridAxis extends ResizeGridItem { + /** Represents a collection of ResizeGridItems along a single axis. + * + * Attributes: + * items (Array[ResizeGridItem]): + * The items contained within this axis. + * item_ids (Object[str, ResizeGridItem]): + * Mapping of item IDs to their ResizeGridItem instance. + */ + constructor(...args) { + super(...args); + this.items = []; + this.item_ids = {}; } destroy() { - this.rows.forEach((row) => { - row.destroy(); - }); - this.rows = null; - if (this.added_outer_row) { - this.elem.innerHTML = this.elem.querySelector( - ':scope > .resize-grid--row' - ).innerHTML; - } + this.items.forEach(item => item.destroy()); + this.items = []; + this.item_ids = {}; super.destroy(); } - addRow(id, elem, row_idx) { - /** Generates a ResizeGridItem and ResizeGridHandle for a row element. */ - const row = new ResizeGridItem({ + addCell(id, elem, idx) { + /** Creates a ResizeGridItem along this axis and returns the new item. */ + const item = new ResizeGridItem({ id: id, parent: this, elem: elem, + axis: this.axis ^ 1, // Children are along the opposite axis of this. callbacks: this.callbacks, - axis: 0, }); - row.genHandle('resize-grid--row-handle'); - row.elem.dataset.row = row_idx; - this.rows.push(row); - this.id_map[id] = row; - return row; + item.genHandle(); + item.elem.dataset.index = idx; + this.items.push(item); + this.item_ids[id] = item; + return item; } - addCol(id, elem, row_idx, col_idx) { - /** Generates a ResizeGridItem and ResizeGridHandle for a column element. */ - const col = new ResizeGridItem({ - id: id, - parent: this, - elem: elem, - callbacks: this.callbacks, - axis: 1, + render({force_flex_grow, reset} = {}) { + /** Renders all children items and this item. */ + if (!this.visible) { + return; + } + + this.items.forEach(item => { + item.render({force_flex_grow: force_flex_grow, reset: reset}); }); - col.genHandle('resize-grid--col-handle'); - col.elem.dataset.row = row_idx; - col.elem.dataset.col = col_idx; - this.grid[row_idx].push(col); - this.id_map[id] = col; - return col; + // If we only have one visible cell, we need to force it to grow to fill axis. + const visible_cells = this.items.filter(x => x.visible); + if (visible_cells.length === 1) { + visible_cells[0].render({force_flex_grow: true}); + } + + if (!(this instanceof ResizeGrid)) { + super.render({force_flex_grow: force_flex_grow, reset: reset}); + } + + this.updateVisibleHandles(); } - build() { - /** Generates rows/cols based on this instance element's content. */ - let row_elems = Array.from(this.elem.querySelectorAll(":scope > .resize-grid--row")); - - // If we do not have any rows, then we generate a single row to contain the cols. - if (!row_elems.length) { - const elem = document.createElement('div'); - elem.classList.add('resize-grid--row'); - elem.append(...this.elem.children); - this.elem.replaceChildren(elem); - row_elems = [elem]; - // track this addition so we can remove it later. - this.added_outer_row = true; - } - - // Make sure that if we only have one row, that it fills the container. - if (row_elems.length === 1 && !row_elems[0].style.flexBasis) { - row_elems[0].style.flexGrow = 1; - row_elems[0].style.flexBasis = - parseInt(this.elem.getBoundingClientRect().height) + 'px'; - } - - let id = 0; - this.grid = [...Array(row_elems.length)].map((_) => []); - row_elems.forEach((row_elem, i) => { - this.addRow(id++, row_elem, i); - const col_elems = row_elem.querySelectorAll('.resize-grid--col'); - if (col_elems.length === 1) { - col_elems[0].style.flexGrow = 1; - } - col_elems.forEach((col_elem, j) => { - this.addCol(id++, col_elem, i, j); - }); - this.grid[i][this.grid[i].length - 1].handle.hide(); - }); - this.rows[this.rows.length - 1].handle.hide(); - - // Now that all handles are added, we need to render the flex styles for each item. - for (let i = 0; i < this.rows.length; i++) { - this.rows[i].render({reset: true}); - for (let j = 0; j < this.grid[i].length; j++) { - this.grid[i][j].render({reset: true}); - } - const vis_cols = this.grid[i].filter((x) => x.visible); - if (vis_cols.length === 1) { - vis_cols[0].render({force_flex_grow: true}); - } - } - const vis_rows = this.rows.filter((x) => x.visible); - if (vis_rows.length === 1) { - vis_rows[0].render({force_flex_grow: true}); + getById(id) { + isStringThrowError(id); + if (id in this.item_ids) { + return this.item_ids[id]; } + throw new Error(`No matching Cell ID in ResizeGridAxis: ${id}`); } getByElem(elem) { - return this.id_map[elem.dataset.id]; + isElementThrowError(elem); + elem = elem.closest(".resize-grid--cell,.resize-grid--col,.resize-grid--row"); + isElementThrowError(elem); + return this.getById(elem.dataset.id); } - getByIdx({row_idx, col_idx} = {}) { - /** Returns the ResizeGridItem at the row/col index. */ - row_idx = parseInt(row_idx); - col_idx = parseInt(col_idx); - if ( - (!isNumber(row_idx) && !isNumber(col_idx)) || - (!isNumber(row_idx) && isNumber(col_idx)) - ) { - console.error('Invalid row/col idx:', row_idx, col_idx); - return; + getByIdx(idx) { + isNumberThrowError(idx); + const res = this.items[idx]; + if (!isNullOrUndefined(res)) { + return res; } - if (isNumber(row_idx) && !isNumber(col_idx)) { - if (row_idx >= this.rows.length) { - console.error( - `row_idx out of range: (${row_idx} > ${this.rows.length})` - ); - return; - } - return this.rows[row_idx]; + throw new Error(`Invalid Cell Index in ResizeGridAxis: ${idx}`); + } + + getItem({id, idx, elem} = {}) { + if (isString(id)) { + return this.getById(id); + } else if (isNumber(idx)) { + return this.getByIdx(idx); + } else if (isElement(elem)) { + return this.getByElem(elem); + } else { + // Indicates programmer error. + throw new Error("Invalid arguments. Must specify one of [id, idx, elem]."); } - if (isNumber(row_idx) && isNumber(col_idx)) { - if (row_idx >= this.grid.length) { - console.error( - `row_idx out of range: (${row_idx} > ${this.grid.length})` - ); - return; - } - if (col_idx >= this.grid[row_idx].length) { - console.error( - `col_idx out of range: (${col_idx} > ${this.grid[row_idx].length})` - ); - return; - } - return this.grid[row_idx][col_idx]; + } + + getSiblings(handle_elem) { + /** Returns the nearest visible ResizeGridItems surrounding a ResizeGridHandle. + * + * Args: + * handle_elem (Element): The handle element in the grid to lookup. + * + * Returns: + * Object: Keys=(prev, next). Values are ResizeGridItems. + */ + let prev = this.getItem({elem: handle_elem.previousElementSibling}); + if (!prev.visible) { + prev = prev.parent.items.slice(0, this.items.indexOf(prev)).findLast(x => x.visible); } + let next = this.getItem({elem: handle_elem.nextElementSibling}); + if (!next.visible) { + next = next.parent.items.slice(this.items.indexOf(next) + 1).findLast(x => x.visible); + } + return {prev: prev, next: next}; } updateVisibleHandles() { /** Sets the visibility of each ResizeGridHandle based on surrounding items. */ - const last_vis_rows_idx = this.rows.findLastIndex((x) => x.visible); - for (let i = 0; i < this.rows.length; i++) { - const last_vis_grid_idx = this.grid[i].findLastIndex((x) => x.visible); - for (let j = 0; j < this.grid[i].length; j++) { - const item = this.getByIdx({row_idx: i, col_idx: j}); - if (isNullOrUndefined(item)) { - continue; - } - - // Don't show handle if item is last column in row. - if (this.grid[i][j].visible && j !== last_vis_grid_idx) { - this.grid[i][j].handle.show(); - } else { - this.grid[i][j].handle.hide(); - } - } - - const item = this.getByIdx({row_idx: i}); - if (isNullOrUndefined(item)) { - continue; - } - - // Don't show handle if item is last row in grid. - if (this.rows[i].visible && i !== last_vis_rows_idx) { - this.rows[i].handle.show(); + for (const item of this.items) { + if (item.visible) { + item.handle.show(); } else { - this.rows[i].handle.hide(); + item.handle.hide(); } } + const last_vis = this.items[this.items.findLastIndex(x => x.visible)]; + if (last_vis instanceof ResizeGridItem) { + last_vis.handle.hide(); + } } - makeRoomForItem(item, siblings, item_idx, {use_base_size} = {}) { - /** Shrinks items along axis until the supplied item can fit. */ + makeRoomForItem(item, siblings, {use_base_size} = {}) { + /** Shrinks items along this axis until the passed item can fit. + * + * Args: + * item (ResizeGridItem): + * The item to be added into this axis. + * siblings (Array[ResizeGridItem]): + * An array of ResizeGridItems within the same container as `item`. + * use_base_size (bool): + * Whether to use the `item`'s base_size or current size when inserting. + * + * Throws: + * Error: If unable to shrink items along this axis to make room for `item`. + */ + isNullOrUndefinedThrowError(item); + const idx = siblings.indexOf(item); + isNumberThrowError(idx); use_base_size = use_base_size === true; - let tot = use_base_size ? item.base_size : item.getSize(); - // Get the item after this item's handle. - let sibling = siblings.slice(item_idx + 1).find((x) => x.visible); - if (isNullOrUndefined(sibling)) { - // No items after this item. Instead get the item just before this item. - sibling = siblings.slice(0, item_idx).findLast((x) => x.visible); - isNullOrUndefinedThrowError(sibling); // Indicates programmer error. - // Last item so we want to hide its handle. - item.handle.hide(); - // Add previous item handle's size - tot += sibling.handle.pad_px; - } else { - // Need to add handle between this item and next item. - item.handle.show(); - tot += item.handle.pad_px; - } + // If use_base_size=false, then try to get the item's current size and use that. + // Since the item is hidden, this will get the size saved in flexBasis which was + // the previously set size. + const tot = (use_base_size ? item.base_size : item.getSize()) + item.handle.pad_px; + let rem = tot; + // Get first visible sibling after the item we're trying to add. + let sibling = siblings.slice(idx + 1).find(x => x.visible); const sibling_idx = siblings.indexOf(sibling); - let rem = tot; - rem = sibling.shrink(rem, {limit_to_base: use_base_size}); + // Shrink from the sibling first. + if (sibling instanceof ResizeGridItem) { + rem = sibling.shrink(rem, {limit_to_base: false}); + } if (rem <= 0) { return; } - // Shrink from flexGrow items next starting from the end. - if (rem > 0) { - const others = siblings.filter( - (x) => - x.visible && x.is_flex_grow && siblings.indexOf(x) !== sibling_idx - ); - for (const other of others.slice().reverse()) { - rem = other.shrink(rem, {limit_to_base: use_base_size}); - if (rem <= 0) { - return; - } - } - } - // Now shrink from non-flexGrow items starting from the end. - if (rem > 0) { - const others = siblings.filter( - (x) => - x.visible && !x.is_flex_grow && siblings.indexOf(x) !== sibling_idx - ); - for (const other of others.slice().reverse()) { - rem = other.shrink(rem, {limit_to_base: use_base_size}); - if (rem <= 0) { - return; - } - } - } - // Shrink the item itself if we still don't have room. - if (rem > 0) { - rem = item.shrink(rem, {limit_to_base: use_base_size}); + // Shrink all other items next, starting from the end. + let others = siblings.filter((x, i) => x.visible && i !== idx && i !== sibling_idx); + for (const other of others.slice().reverse()) { + rem = other.shrink(rem, {limit_to_base: false}); if (rem <= 0) { return; } } - // If still not enough room, try again but use the base sizes. - if (rem > 0 && !use_base_size) { - this.makeRoomForItem(item, siblings, item_idx, {use_base_size: true}); + // Finally, shrink from the item itself since it is too big to insert. + rem = item.shrink(rem, {limit_to_base: false}); + if (rem <= 0) { return; } - // If we still couldn't make room, this indicates programmer error. - throw new Error(`No space for row. tot: ${tot}, rem: ${rem}`); + + // This indicates a programmer error. + throw new Error(`No space for item. tot: ${tot}, rem: ${rem}`); } - growToFill(item, siblings, item_idx, tot_px) { - /** Expands item along axis until the axis has no remaining space. */ - // Expand the item that was attached via the hidden item's handle first. - let sibling = siblings.slice(item_idx + 1).find((x) => x.visible); + growToFill(item, siblings) { + /** Grows an item along an axis to fill its container. + * + * Args: + * item (ResizeGridItem): + * The item to grow. + * siblings (Array[ResizeGridItem]): + * An array of ResizeGridItems within the same container as `item`. + */ + const idx = siblings.indexOf(item); + isNullOrUndefinedThrowError(item); // Indicates programmer error. + let sibling = siblings.slice(idx + 1).find(x => x.visible); if (isNullOrUndefined(sibling)) { - // Otherwise, expand the previous attached item. - sibling = siblings.slice(0, item_idx).findLast((x) => x.visible); + sibling = siblings.slice(0, idx).findLast(x => x.visible); isNullOrUndefinedThrowError(sibling); // Indicates programmer error. - } else { - tot_px += item.pad_px; } - // Hide sibling's handle if sibling is last visible item. - if ( - siblings - .slice(siblings.findIndex((x) => x === sibling) + 1) - .every((x) => !x.visible) - ) { - if (sibling.handle.visible) { - sibling.handle.hide(); - tot_px += sibling.pad_px; - } + + const sibling_idx = siblings.indexOf(sibling); + + // Hide sibling's handle if sibling is the last visible item. + if (siblings.slice(sibling_idx + 1).every(x => !x.visible) && sibling.handle.visible) { + sibling.handle.hide(); } // If we are growing sibling to fill, then just set flexGrow=1. - if ( - siblings.length <= 2 || - siblings.every((x) => !x.visible && x !== sibling && x !== item) - ) { + if (siblings.length <= 2 || siblings.every(x => !x.visible && x !== sibling && x !== item)) { sibling.render({force_flex_grow: true}); } else { sibling.grow(-1); } } - showRow(row_idx, {show_empty_row} = {}) { - /** Makes space for the row then shows it. */ - show_empty_row = show_empty_row === true; - row_idx = parseInt(row_idx); - const item = this.getByIdx({row_idx: row_idx}); - isNullOrUndefinedThrowError(item); + show({id, idx, elem, item} = {}) { + /** Shows an item along this axis. + * + * The arguments to this function are used to lookup the item to show. + */ + if (!(item instanceof ResizeGridItem)) { + item = this.getItem({id: id, idx: idx, elem: elem}); + } if (item.visible) { return; } - if (item.axis !== 0) { - console.error('Expected row, got col:', item); - return; + // We are trying to show an item but the container (this) is not visible. + // Show the container (this) first so we can show the item. + if (item !== this && !this.visible) { + // If any parent items are visible, then we need to make room for this item. + if (this.parent.items.some(x => x.visible)) { + this.makeRoomForItem(this, this.parent.items, {use_base_size: false}); + } + super.show(); + this.parent.render(); } - // If no columns are visible, then we can't show the row. - if (this.grid[row_idx].every((x) => !x.visible) && !show_empty_row) { - console.error('No visible columns in row. Cannot show.'); - return; - } - - // All rows are hidden. We just show this row and make it fill the container. - if (this.rows.every((x) => !x.visible)) { + // No items are visible in this container. Show the item and force flexGrow=1. + if (this.items.every(x => !x.visible)) { item.show(); item.handle.hide(); item.render({force_flex_grow: true}); - } else { - this.makeRoomForItem(item, this.rows, row_idx); - item.show(); - if (this.rows.slice(row_idx + 1).every((x) => !x.visible)) { - // If this is the last visible row, hide the handle. - item.handle.hide(); - } - const prev_item = this.rows.slice(0, row_idx).find((x) => x.visible); - if (!isNullOrUndefined(prev_item)) { - prev_item.handle.show(); - } - item.render(); - } - } - - showCol(row_idx, col_idx) { - /** Makes space for the column then shows it. */ - row_idx = parseInt(row_idx); - col_idx = parseInt(col_idx); - const item = this.getByIdx({row_idx: row_idx, col_idx: col_idx}); - isNullOrUndefinedThrowError(item); - - if (item.visible) { return; } - if (item.axis !== 1) { - console.error('Expected col, got row:', item); - return; - } - - // If the row isn't visible, we need to show it before we can show columns. - if (!this.rows[row_idx].visible) { - this.showRow(row_idx, {show_empty_row: true}); - } - - // All cols are hidden. We just show this col and make it fill the row. - if (this.grid[row_idx].every((x) => !x.visible)) { - item.show(); - item.handle.hide(); - item.render({force_flex_grow: true}); - } else { - this.makeRoomForItem(item, this.grid[row_idx], col_idx); - item.show(); - if (this.grid[row_idx].slice(col_idx + 1).every((x) => !x.visible)) { - // If this is the last visible col, hide the handle. - item.handle.hide(); - } - const prev_item = this.grid[row_idx] - .slice(0, col_idx) - .find((x) => x.visible); - if (!isNullOrUndefined(prev_item)) { - prev_item.handle.show(); - } - item.render(); - } + // Other items are visible in this container, make room for this item. + this.makeRoomForItem(item, this.items, {use_base_size: false}); + item.show(); + this.updateVisibleHandles(); } - hideRow(row_idx) { - /** Hides a row and resizes other rows to fill the gap. */ - row_idx = parseInt(row_idx); - const item = this.getByIdx({row_idx: row_idx}); - isNullOrUndefinedThrowError(item); + hide({id, idx, elem, item} = {}) { + /** Hides an item along this axis. + * + * The arguments to this function are used to lookup the item to hide. + */ + if (!(item instanceof ResizeGridItem)) { + item = this.getItem({id: id, idx: idx, elem: elem}); + } if (!item.visible) { return; } - if (item.axis !== 0) { - console.error('Expected row, got col:', item); - return; - } - - let tot_px = item.elem.getBoundingClientRect().height; item.hide(); - // If no other rows are visible, we don't need to do anything else. - if (this.rows.every((x) => !x.visible)) { + // If no other items are visible, hide the container. + if (this.items.every(x => !x.visible)) { + super.hide(); + if (this.parent.items.every(x => !x.visible)) { + this.parent.render(); + return; + } + this.parent.growToFill(this, this.parent.items); + this.parent.render(); return; } - this.growToFill(item, this.rows, row_idx, tot_px); + this.growToFill(item, this.items); } - hideCol(row_idx, col_idx) { - /** Hides a column and resizes other columns to fill the gap. */ - row_idx = parseInt(row_idx); - col_idx = parseInt(col_idx); - const item = this.getByIdx({row_idx: row_idx, col_idx: col_idx}); - isNullOrUndefinedThrowError(item); - - if (!item.visible) { - return; + toggle({id, idx, elem, item, override} = {}) { + /** Toggles the visibility of an item along this axis. + * + * The arguments to this function are used to lookup the item to show. + * + * Args: + * override (bool): If specified, this value is used to set visibility. + */ + if (!(item instanceof ResizeGridItem)) { + item = this.getItem({id: id, idx: idx, elem: elem}); } - if (item.axis !== 1) { - console.error('Expected col, got row:', item); - return; - } - - let tot_px = item.getSize(); - item.hide(); - // If no other cols are visible, hide the containing row. - if (this.grid[row_idx].every((x) => !x.visible)) { - this.hideRow(row_idx); - return; - } - - this.growToFill(item, this.grid[row_idx], col_idx, tot_px); - } - - show({row_idx, col_idx} = {}) { - /** Shows a row or column based on the provided row/col indices. */ - row_idx = parseInt(row_idx); - col_idx = parseInt(col_idx); - if (isNumber(row_idx) && !isNumber(col_idx)) { - this.showRow(row_idx); - } else if (isNumber(row_idx) && isNumber(col_idx)) { - this.showCol(row_idx, col_idx); - } else { - throw new Error('Invalid parameters for row/col idx:', row_idx, col_idx); - } - } - - hide({row_idx, col_idx} = {}) { - /** Hides a row or column based on the provided row/col indices. */ - row_idx = parseInt(row_idx); - col_idx = parseInt(col_idx); - if (isNumber(row_idx) && !isNumber(col_idx)) { - this.hideRow(row_idx); - } else if (isNumber(row_idx) && isNumber(col_idx)) { - this.hideCol(row_idx, col_idx); - } else { - throw new Error('Invalid parameters for row/col idx:', row_idx, col_idx); - } - } - - toggle({row_idx, col_idx, override} = {}) { - /** Toggles a row or column's visibility based on the provided row/col indices. */ - row_idx = parseInt(row_idx); - col_idx = parseInt(col_idx); - const item = this.getByIdx({row_idx: row_idx, col_idx: col_idx}); - isNullOrUndefinedThrowError(item); - let new_state = !item.visible; if (override === true || override === false) { new_state = override; } - - if (item.axis === 0) { - new_state ? this.showRow(row_idx) : this.hideRow(row_idx); - } else { - new_state ? - this.showCol(row_idx, col_idx) : - this.hideCol(row_idx, col_idx); - } + new_state ? item.parent.show({item: item}) : item.parent.hide({item: item}); } } -class ResizeGrid { - /** Class representing a resizable grid. */ +class ResizeGrid extends ResizeGridAxis { + /** Class representing a resizable grid. + * + * Attributes (the less obvious ones): + * added_outer_div (bool): + * Whether the outermost ResizeGridAxis was added during setup. This is + * used on destruction to revert the container element to its original state. + * prev_dims (Object): + * Generated by elem.getBoundingClientRect(). This tracks the last known + * dimensions of this container element between resize events. + */ event_abort_controller = null; - added_outer_row = false; - container = null; + added_outer_div = false; setup_has_run = false; prev_dims = null; resize_observer = null; - resize_observer_timer; - constructor(id, elem, {callbacks}={}) { - this.id = id; - this.elem = elem; - this.elem.dataset.gridId = this.id; - this.callbacks = callbacks; + resize_observer_timer = null; + constructor(id, elem, {callbacks} = {}) { + const row_elems = Array.from(elem.querySelectorAll(":scope > .resize-grid--row")); + const col_elems = Array.from(elem.querySelectorAll(":scope > .resize-grid--col")); + let axis = 0; + if (row_elems.length && col_elems.length) { + throw new Error("Invalid grid. Cannot have rows and cols at same level."); + } else if (row_elems.length) { + axis = 0; + } else if (col_elems.length) { + axis = 1; + } else { + axis = 0; + } + + super({ + id: id, + elem: elem, + parent: null, + axis: axis, + callbacks: callbacks, + }); } destroy() { this.destroyEvents(); - if (!isNullOrUndefined(this.container)) { - this.container.destroy(); - this.container = null; + if (this.added_outer_div) { + this.elem.innerHTML = this.elem.children[0].innerHTML; + this.added_outer_div = false; } + super.destroy(); this.setup_has_run = false; } + destroyEvents() { + /** Destroys all event listeners and observers. */ + // We can simplify removal of event listeners by firing an AbortController + // abort signal. Must pass the signal to any event listeners on creation. + if (this.event_abort_controller) { + this.event_abort_controller.abort(); + } + if (!isNullOrUndefined(this.resize_observer)) { + this.resize_observer.disconnect(); + } + clearTimeout(this.resize_observer_timer); + this.resize_observer = null; + this.resize_observer_timer = null; + } + setup() { /** Fully prepares this instance for use. */ - if (!this.elem.querySelector('.resize-grid--row,.resize-grid--col')) { - throw new Error('Container has no rows or cols.'); + // We don't want to run setup a second time without having run `destroy()`. + if (this.setup_has_run) { + // Indicates programmer error. + throw new Error("Setup has already run."); } - if (!isNullOrUndefined(this.container)) { - this.container.destroy(); - this.container = null; + if (!this.elem.querySelector('.resize-grid--row,.resize-grid--col,.resize-grid--cell')) { + throw new Error('Grid has no valid content from which it can build.'); } - this.container = new ResizeGridContainer({ - parent: this, - elem: this.elem, - callbacks: this.callbacks, - }); - this.container.build(); this.prev_dims = this.elem.getBoundingClientRect(); + this.build(); this.setupEvents(); this.setup_has_run = true; } @@ -855,46 +853,21 @@ class ResizeGrid { } } - const elem = event.target.closest('.resize-grid--handle'); - if (!elem) { + const handle_elem = event.target.closest('.resize-grid--handle'); + if (!isElement(handle_elem)) { return; } // Clicked handles will always be between two elements. If the user // somehow clicks an invisible handle then we have bigger problems. - prev = this.container.getByElem(elem.previousElementSibling); - if (!prev.visible) { - const row_idx = prev.elem.dataset.row; - const col_idx = prev.elem.dataset.col; - const siblings = - prev.axis === 0 ? - this.container.rows : - this.container.grid[row_idx]; - const idx = prev.axis === 0 ? row_idx : col_idx; - prev = siblings.slice(0, idx).findLast((x) => x.visible); - } - handle = prev.handle; - next = this.container.getByElem(elem.nextElementSibling); - if (!next.visible) { - const row_idx = next.elem.dataset.row; - const col_idx = next.elem.dataset.col; - const siblings = - next.axis === 0 ? - this.container.rows : - this.container.grid[row_idx]; - const idx = next.axis === 0 ? row_idx : col_idx; - next = siblings.slice(idx).find((x) => x.visible); - } - - if ( - isNullOrUndefinedLogError(prev) || - isNullOrUndefinedLogError(handle) || - isNullOrUndefinedLogError(next) + const siblings = this.getSiblings(handle_elem); + if (!(siblings.prev instanceof ResizeGridItem) || + !(siblings.next instanceof ResizeGridItem) ) { - prev = null; - handle = null; - next = null; - return; + throw new Error("Failed to find siblings for ResizeGridHandle."); } + prev = siblings.prev; + handle = prev.handle; + next = siblings.next; event.preventDefault(); event.stopPropagation(); @@ -903,6 +876,8 @@ class ResizeGrid { // Temporarily set styles for elements. These are cleared on pointerup. // See `onMove()` comments for more info. + prev.setSize(prev.getSize()); + next.setSize(next.getSize()); prev.elem.style.flexGrow = 0; next.elem.style.flexGrow = 1; next.elem.style.flexShrink = 1; @@ -970,10 +945,8 @@ class ResizeGrid { // Set the new flexBasis value for the `next` element then revert // the style changes set in the `pointerup` event. next.elem.style.flexBasis = next.setSize(next.getSize()); - prev.elem.style.flexGrow = Number(prev.is_flex_grow); - next.elem.style.flexGrow = Number(next.is_flex_grow); - next.elem.style.flexShrink = 0; - + prev.render(); + next.render(); document.body.classList.remove('resizing'); document.body.classList.remove('resizing-col'); @@ -1030,19 +1003,93 @@ class ResizeGrid { this.resize_observer.observe(this.elem); } - destroyEvents() { - /** Destroys all event listeners and observers. */ - // We can simplify removal of event listeners by firing an AbortController - // abort signal. Must pass the signal to any event listeners on creation. - if (this.event_abort_controller) { - this.event_abort_controller.abort(); + addAxis(id, elem, idx) { + /** Creates a ResizeGridAxis instance and returns the new item. */ + const item = new ResizeGridAxis({ + id: id, + parent: this, + elem: elem, + axis: this.axis, + callbacks: this.callbacks, + }); + item.elem.dataset.index = idx; + item.genHandle(); + this.items.push(item); + return item; + } + + build() { + /** Generates rows/cols based on this instance element's content. */ + // Flex direction should be opposite of container axis. + this.elem.style.flexDirection = this.axis === 0 ? "column" : "row"; + let axis_elems; + if (this.axis === 0) { + axis_elems = Array.from(this.elem.querySelectorAll(":scope > .resize-grid--row")); + } else { + axis_elems = Array.from(this.elem.querySelectorAll(":scope > .resize-grid--col")); } - if (!isNullOrUndefined(this.resize_observer)) { - this.resize_observer.disconnect(); + if (!axis_elems.length) { + // If we don't have any rows/cols, then make a new axis. + const elem = document.createElement("div"); + elem.classList.add("resize-grid--row"); + elem.append(...this.elem.children); + axis_elems = [elem]; + // track this change so we can undo it on `destroy()`. + this.added_outer_div = true; + } + + // If we only have a single row/col then make it fill the container. + if (axis_elems.length === 1) { + axis_elems[0].style.flexGrow = 1; + } + let id = 0; + axis_elems.forEach((axis_elem, i) => { + const axis = this.addAxis(id++, axis_elem, i); + axis_elem.querySelectorAll( + ":scope > .resize-grid--cell" + ).forEach((cell_elem, j) => axis.addCell(id++, cell_elem, j)); + }); + + // Set any rows/cols with only one element to be flexGrow=1 and flexShrink=1 + // by default. Since they don't have siblings to work around, this makes it so + // the user doesn't have to specify these settings in HTML. + if (this.items.length === 1) { + this.items[0].is_flex_grow = 1; + this.items[0].elem.style.flexGrow = 1; + this.items[0].is_flex_shrink = 1; + this.items[0].elem.style.flexShrink = 1; + } + + for (const item of this.items) { + if (item.items.length === 1) { + item.items[0].is_flex_grow = 1; + item.items[0].elem.style.flexGrow = 1; + item.items[0].is_flex_shrink = 1; + item.items[0].elem.style.flexShrink = 1; + } + } + + // Render all axes to force them to update flex dims. + this.items[this.items.length - 1].handle.hide(); + this.items.forEach(axis => { + axis.items[axis.items.length - 1].handle.hide(); + }); + this.render({reset: true}); + this.buildIdMap(this); + } + + buildIdMap(item) { + /** Generates a mapping from ID to ResizeGridItem instances. + * + * Starts at the passed `item` as a root and recursively builds mapping + * from child items. + */ + if (item instanceof ResizeGridAxis) { + this.item_ids[item.id] = item; + item.items.forEach(x => this.buildIdMap(x)); + } else if (item instanceof ResizeGridItem) { + this.item_ids[item.id] = item; } - clearTimeout(this.resize_observer_timer); - this.resize_observer = null; - this.resize_observer_timer = null; } onMove(event, a, handle, b) { @@ -1079,26 +1126,39 @@ class ResizeGrid { if (d_w < 0) { // Width decrease - for (const row of this.container.grid) { - let rem = Math.abs(d_w); - for (const col of row.slice().reverse()) { - const flex_grow = parseInt(col.elem.style.flexGrow); - rem = col.shrink(rem, {limit_to_base: false}); - // Shrink causes flexGrow to be set back to default. - // We want to keep the current flex grow setting so we set it back. - col.render({force_flex_grow: flex_grow === 0 ? false : true}); - if (rem <= 0) { - break; + for (const item of this.items) { + if (this.axis === 0) { + for (const subitem of item.items) { + if (subitem.getSize() > subitem.min_size) { + subitem.elem.style.flexShrink = 1; + } else { + subitem.elem.style.flexShrink = 0; + subitem.elem.style.flexBasis = subitem.min_size + "px"; + } + } + } else { + if (item.getSize() > item.min_size) { + item.elem.style.flexShrink = 1; + } else { + item.elem.style.flexShrink = 0; + item.elem.style.flexBasis = item.min_size + "px"; } } } } else if (d_w > 0) { // width increase - for (const row of this.container.grid) { - for (const col of row.slice().reverse()) { - const amt = col.grow(-1, {only_if_flex: true}); - if (amt === 0) { - break; + if (this.axis === 0) { + for (const item of this.items) { + let did_grow = false; + let subitem = item.items.findLast(x => x.visible && x.is_flex_grow); + if (subitem instanceof ResizeGridItem) { + did_grow = subitem.grow(-1) === 0; + } + if (!did_grow) { + subitem = item.items.findLast(x => x.visible && !x.is_flex_grow); + if (subitem instanceof ResizeGridItem) { + subitem.grow(-1); + } } } } @@ -1106,84 +1166,55 @@ class ResizeGrid { if (d_h < 0) { // height decrease - let rem = Math.abs(d_h); - for (const row of this.container.rows.slice().reverse()) { - const flex_grow = parseInt(row.elem.style.flexGrow); - rem = row.shrink(rem, {limit_to_base: false}); - // Shrink causes flexGrow to be set back to default. - // We want to keep the current flex grow setting so we set it back. - row.render({force_flex_grow: flex_grow === 0 ? false : true}); - if (rem <= 0) { - break; + for (const item of this.items) { + if (this.axis === 1) { + for (const subitem of item.items) { + if (subitem.getSize() > subitem.min_size) { + subitem.elem.style.flexShrink = 1; + } else { + subitem.elem.style.flexShrink = 0; + subitem.elem.style.flexBasis = subitem.min_size + "px"; + } + } + } else { + if (item.getSize() > item.min_size) { + item.elem.style.flexShrink = 1; + } else { + item.elem.style.flexShrink = 0; + item.elem.style.flexBasis = item.min_size + "px"; + } } } } else if (d_h > 0) { // height increase - for (const row of this.container.rows.slice().reverse()) { - if (row.grow(-1, {only_if_flex: true}) === 0) { - break; + if (this.axis === 1) { + let did_grow = false; + let item = this.items.findLast(x => x.visible && x.is_flex_grow); + if (item instanceof ResizeGridItem) { + did_grow = item.grow(-1) === 0; + } + if (!did_grow) { + item = this.items.findLast(x => x.visible && !x.is_flex_grow); + if (item instanceof ResizeGridItem) { + item.grow(-1); + } } } } this.prev_dims = curr_dims; } - - show({row_idx, col_idx} = {}) { - /** Show a row or column. - * Columns require both the row_idx and col_idx. - */ - this.container.show({row_idx: row_idx, col_idx: col_idx}); - this.container.updateVisibleHandles(); - } - - hide({row_idx, col_idx} = {}) { - /** Hide a row or column. - * Columns require both the row_idx and col_idx. - */ - this.container.hide({row_idx: row_idx, col_idx: col_idx}); - this.container.updateVisibleHandles(); - } - - toggle({row_idx, col_idx, override} = {}) { - /** Toggle visibility of a row or column. - * Columns require both the row_idx and col_idx. - */ - this.container.toggle({ - row_idx: row_idx, - col_idx: col_idx, - override: override, - }); - this.container.updateVisibleHandles(); - } - - toggleElem(elem, override) { - /** Toggles the nearest ResizeGridItem to the passed element. */ - isElementThrowError(elem); - let _elem = elem.closest('.resize-grid--col'); - if (isElement(_elem)) { - this.toggle({ - row_idx: _elem.dataset.row, - col_idx: _elem.dataset.col, - override: override, - }); - } else { - _elem = elem.closest('.resize-grid--row'); - isElementThrowError(_elem); - this.toggle({row_idx: _elem.dataset.row, override: override}); - } - } } function resizeGridGetGrid(elem) { - /** Returns the nearest ResizeGrid to the passed element. */ + /** Returns the nearest ResizeGrid instance to the passed element. */ // Need to find grid element so we can lookup by its id. const grid_elem = elem.closest('.resize-grid'); if (!isElement(grid_elem)) { return null; } // Now try to get the actual ResizeGrid instance. - const grid = resize_grids[grid_elem.dataset.gridId]; + const grid = resize_grids[grid_elem.dataset.id]; if (isNullOrUndefined(grid)) { return null; } diff --git a/javascript/resizeGridExample.html b/javascript/resizeGridExample.html new file mode 100644 index 000000000..2f10ce074 --- /dev/null +++ b/javascript/resizeGridExample.html @@ -0,0 +1,178 @@ + + + + + + + +
+
+
+
+
0
+
+
+
1
+
+
+
+
+
2
+
+
+
3
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/javascript/utils.js b/javascript/utils.js index f21fbb0cb..cf499420d 100644 --- a/javascript/utils.js +++ b/javascript/utils.js @@ -169,6 +169,25 @@ function querySelectorThrowError(selector) { 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; diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 68aee4877..cc19865ad 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -862,17 +862,17 @@ class ExtraNetworksPage: if not tree_view_en and not card_view_en and not dets_view_en: main_row_data_attributes += "" - tree_view_col_data_attributes = "data-min-size=\"10vw\"" + tree_view_cell_data_attributes = "data-min-size=\"10vw\"" if not tree_view_en: - tree_view_col_data_attributes += "" + tree_view_cell_data_attributes += "" - card_view_col_data_attributes = "data-min-size=\"10vw\"" + card_view_cell_data_attributes = "data-min-size=\"10vw\"" if not card_view_en: - card_view_col_data_attributes += "" + card_view_cell_data_attributes += "" - dets_view_col_data_attributes = "data-min-size=\"10vw\"" + dets_view_cell_data_attributes = "data-min-size=\"10vw\"" if not dets_view_en: - dets_view_col_data_attributes += "" + dets_view_cell_data_attributes += "" dirs_view_row_size = shared.opts.extra_networks_dirs_view_default_height tree_view_size = shared.opts.extra_networks_tree_view_default_width @@ -884,9 +884,9 @@ class ExtraNetworksPage: dirs_view_row_style = f"flex: 0 0 {dirs_view_row_size}px;" main_row_style = "flex: 1 0 0px;" # expand to fill - tree_view_col_style = f"flex: 0 0 {tree_view_size}px;" - card_view_col_style = f"flex: 1 0 {card_view_size}px;" - dets_view_col_style = f"flex: 0 0 {dets_view_size}px;" + tree_view_cell_style = f"flex: 0 0 {tree_view_size}px;" + card_view_cell_style = f"flex: 1 0 {card_view_size}px;" + dets_view_cell_style = f"flex: 0 0 {dets_view_size}px;" return self.pane_tpl.format( tabname=tabname, @@ -903,14 +903,14 @@ class ExtraNetworksPage: btn_dets_view_data_attributes=btn_dets_view_data_attributes, dirs_view_row_style=dirs_view_row_style, main_row_style=main_row_style, - tree_view_col_style=tree_view_col_style, - card_view_col_style=card_view_col_style, - dets_view_col_style=dets_view_col_style, + tree_view_cell_style=tree_view_cell_style, + card_view_cell_style=card_view_cell_style, + dets_view_cell_style=dets_view_cell_style, dirs_view_row_data_attributes=dirs_view_row_data_attributes, main_row_data_attributes=main_row_data_attributes, - tree_view_col_data_attributes=tree_view_col_data_attributes, - card_view_col_data_attributes=card_view_col_data_attributes, - dets_view_col_data_attributes=dets_view_col_data_attributes, + tree_view_cell_data_attributes=tree_view_cell_data_attributes, + card_view_cell_data_attributes=card_view_cell_data_attributes, + dets_view_cell_data_attributes=dets_view_cell_data_attributes, dirs_html=dirs_html, card_list_loading_splash_content=card_list_loading_splash_content, card_list_no_data_splash_content=card_list_no_data_splash_content, diff --git a/style.css b/style.css index 2fea5fcd4..1d39f90c0 100644 --- a/style.css +++ b/style.css @@ -1201,7 +1201,7 @@ body.resizing.resize-grid-row { /* ==== RESIZE HANDLE GRID ==== */ -.resize-grid, .resize-grid--row, .resize-grid--col { +.resize-grid, .resize-grid--row, .resize-grid--col, .resize-grid--cell { display: flex; flex-wrap: nowrap; overflow: hidden; @@ -1225,11 +1225,6 @@ body.resizing.resize-grid-row { flex-direction: column; } -.resize-grid--cell { - display: block; - position: relative; -} - .resize-grid--handle { position: relative; height: 100%;