From d81c8f8aad66e01a3cc6c2bcb43f0d5a1842b37d Mon Sep 17 00:00:00 2001 From: Marvin Heilemann <11534760+muuvmuuv@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:41:01 +0200 Subject: [PATCH 1/5] Allow nested resize handler --- src/dd-resizable-handle.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/dd-resizable-handle.ts b/src/dd-resizable-handle.ts index d4137e4c..f22904ba 100644 --- a/src/dd-resizable-handle.ts +++ b/src/dd-resizable-handle.ts @@ -34,12 +34,15 @@ export class DDResizableHandle { /** @internal */ protected _init(): DDResizableHandle { - const el = this.el = document.createElement('div'); - el.classList.add('ui-resizable-handle'); - el.classList.add(`${DDResizableHandle.prefix}${this.dir}`); - el.style.zIndex = '100'; - el.style.userSelect = 'none'; - this.host.appendChild(this.el); + this.el = this.host.querySelector(".ui-resizable-handle") + if (!this.el) { + this.el = document.createElement('div'); + this.el.classList.add('ui-resizable-handle'); + this.host.appendChild(this.el); + } + this.el.classList.add(`${DDResizableHandle.prefix}${this.dir}`); + this.el.style.zIndex = '100'; + this.el.style.userSelect = 'none'; this.el.addEventListener('mousedown', this._mouseDown); if (isTouch) { this.el.addEventListener('touchstart', touchstart); From edbddbc27c02ba3a44f22c997d0fb5d74df60932 Mon Sep 17 00:00:00 2001 From: Marvin Heilemann <11534760+muuvmuuv@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:58:13 +0200 Subject: [PATCH 2/5] Fixed error when element is not a child of this node --- src/dd-resizable-handle.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dd-resizable-handle.ts b/src/dd-resizable-handle.ts index f22904ba..68a69f06 100644 --- a/src/dd-resizable-handle.ts +++ b/src/dd-resizable-handle.ts @@ -60,7 +60,16 @@ export class DDResizableHandle { this.el.removeEventListener('touchstart', touchstart); this.el.removeEventListener('pointerdown', pointerdown); } - this.host.removeChild(this.el); + try { + this.host.removeChild(this.el); + } catch (error) { + if (error instanceof Error) { + if (error.name !== "NotFoundError") { + // Skip if handle is not a child (created in _init) of the parent node + throw error + } + } + } delete this.el; delete this.host; return this; From 50a3f2a01b942cb0fb660bea43aae05c1add45a4 Mon Sep 17 00:00:00 2001 From: Marvin Heilemann <11534760+muuvmuuv@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:09:44 +0200 Subject: [PATCH 3/5] Use an resize option instead for better compatibility --- src/dd-resizable-handle.ts | 20 +- src/dd-resizable.ts | 2 + src/types.ts | 1147 ++++++++++++++++++------------------ 3 files changed, 586 insertions(+), 583 deletions(-) diff --git a/src/dd-resizable-handle.ts b/src/dd-resizable-handle.ts index 68a69f06..9b0ba282 100644 --- a/src/dd-resizable-handle.ts +++ b/src/dd-resizable-handle.ts @@ -4,9 +4,9 @@ */ import { isTouch, pointerdown, touchend, touchmove, touchstart } from './dd-touch'; -import { GridItemHTMLElement } from './gridstack'; +import { DDResizableOpt, GridItemHTMLElement } from './gridstack'; -export interface DDResizableHandleOpt { +export interface DDResizableHandleOpt extends DDResizableOpt { start?: (event) => void; move?: (event) => void; stop?: (event) => void; @@ -34,8 +34,11 @@ export class DDResizableHandle { /** @internal */ protected _init(): DDResizableHandle { - this.el = this.host.querySelector(".ui-resizable-handle") - if (!this.el) { + if (this.option.element) { + this.el = this.option.element instanceof HTMLElement + ? this.option.element + : this.host.querySelector(this.option.element) + } else { this.el = document.createElement('div'); this.el.classList.add('ui-resizable-handle'); this.host.appendChild(this.el); @@ -60,15 +63,8 @@ export class DDResizableHandle { this.el.removeEventListener('touchstart', touchstart); this.el.removeEventListener('pointerdown', pointerdown); } - try { + if (!this.option.element) { this.host.removeChild(this.el); - } catch (error) { - if (error instanceof Error) { - if (error.name !== "NotFoundError") { - // Skip if handle is not a child (created in _init) of the parent node - throw error - } - } } delete this.el; delete this.host; diff --git a/src/dd-resizable.ts b/src/dd-resizable.ts index 2f1f3ca3..fe48071a 100644 --- a/src/dd-resizable.ts +++ b/src/dd-resizable.ts @@ -15,6 +15,7 @@ import { DDManager } from './dd-manager'; export interface DDResizableOpt { autoHide?: boolean; handles?: string; + element?: string | HTMLElement; maxHeight?: number; maxHeightMoveUp?: number; maxWidth?: number; @@ -153,6 +154,7 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt this.handlers = this.option.handles.split(',') .map(dir => dir.trim()) .map(dir => new DDResizableHandle(this.el, dir, { + ...this.option, start: (event: MouseEvent) => { this._resizeStart(event); }, diff --git a/src/types.ts b/src/types.ts index cd48102b..94dafba6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,571 +1,576 @@ -/** - * types.ts 12.3.3 - * Copyright (c) 2021-2025 Alain Dumesny - see GridStack root license - */ - -import { GridStack } from './gridstack'; -import { GridStackEngine } from './gridstack-engine'; - -/** - * Default values for grid options - used during initialization and when saving out grid configuration. - * These values are applied when options are not explicitly provided. - */ -export const gridDefaults: GridStackOptions = { - alwaysShowResizeHandle: 'mobile', - animate: true, - auto: true, - cellHeight: 'auto', - cellHeightThrottle: 100, - cellHeightUnit: 'px', - column: 12, - draggable: { handle: '.grid-stack-item-content', appendTo: 'body', scroll: true }, - handle: '.grid-stack-item-content', - itemClass: 'grid-stack-item', - margin: 10, - marginUnit: 'px', - maxRow: 0, - minRow: 0, - placeholderClass: 'grid-stack-placeholder', - placeholderText: '', - removableOptions: { accept: 'grid-stack-item', decline: 'grid-stack-non-removable'}, - resizable: { handles: 'se' }, - rtl: 'auto', - - // **** same as not being set **** - // disableDrag: false, - // disableResize: false, - // float: false, - // handleClass: null, - // removable: false, - // staticGrid: false, - //removable -}; - -/** - * Different layout options when changing the number of columns. - * - * These options control how widgets are repositioned when the grid column count changes. - * Note: The new list may be partially filled if there's a cached layout for that size. - * - * Options: - * - `'list'`: Treat items as a sorted list, keeping them sequentially without resizing (unless too big) - * - `'compact'`: Similar to list, but uses compact() method to fill empty slots by reordering - * - `'moveScale'`: Scale and move items by the ratio of newColumnCount / oldColumnCount - * - `'move'`: Only move items, keep their sizes - * - `'scale'`: Only scale items, keep their positions - * - `'none'`: Leave items unchanged unless they don't fit in the new column count - * - Custom function: Provide your own layout logic - */ -export type ColumnOptions = 'list' | 'compact' | 'moveScale' | 'move' | 'scale' | 'none' | - ((column: number, oldColumn: number, nodes: GridStackNode[], oldNodes: GridStackNode[]) => void); -/** - * Options for the compact() method to reclaim empty space. - * - `'list'`: Keep items in order, move them up sequentially - * - `'compact'`: Find truly empty spaces, may reorder items for optimal fit - */ -export type CompactOptions = 'list' | 'compact'; -/** - * Type representing values that can be either numbers or strings (e.g., dimensions with units). - * Used for properties like width, height, margins that accept both numeric and string values. - */ -export type numberOrString = number | string; -/** - * Extended HTMLElement interface for grid items. - * All grid item DOM elements implement this interface to provide access to their grid data. - */ -export interface GridItemHTMLElement extends HTMLElement { - /** Pointer to the associated grid node instance containing position, size, and other widget data */ - gridstackNode?: GridStackNode; - /** @internal Original node data (used for restoring during drag operations) */ - _gridstackNodeOrig?: GridStackNode; -} - -/** - * Type representing various ways to specify grid elements. - * Can be a CSS selector string, HTMLElement, or GridItemHTMLElement. - */ -export type GridStackElement = string | HTMLElement | GridItemHTMLElement; - -/** - * Event handler function types for the .on() method. - * Different handlers receive different parameters based on the event type. - */ - -/** General event handler that receives only the event */ -export type GridStackEventHandler = (event: Event) => void; - -/** Element-specific event handler that receives event and affected element */ -export type GridStackElementHandler = (event: Event, el: GridItemHTMLElement) => void; - -/** Node-based event handler that receives event and array of affected nodes */ -export type GridStackNodesHandler = (event: Event, nodes: GridStackNode[]) => void; - -/** Drop event handler that receives previous and new node states */ -export type GridStackDroppedHandler = (event: Event, previousNode: GridStackNode, newNode: GridStackNode) => void; - -/** Union type of all possible event handler types */ -export type GridStackEventHandlerCallback = GridStackEventHandler | GridStackElementHandler | GridStackNodesHandler | GridStackDroppedHandler; - -/** - * Optional callback function called during load() operations. - * Allows custom handling of widget addition/removal for framework integration. - * - * @param parent - The parent HTML element - * @param w - The widget definition - * @param add - True if adding, false if removing - * @param grid - True if this is a grid operation - * @returns The created/modified HTML element, or undefined - */ -export type AddRemoveFcn = (parent: HTMLElement, w: GridStackWidget, add: boolean, grid: boolean) => HTMLElement | undefined; - -/** - * Optional callback function called during save() operations. - * Allows adding custom data to the saved widget structure. - * - * @param node - The internal grid node - * @param w - The widget structure being saved (can be modified) - */ -export type SaveFcn = (node: GridStackNode, w: GridStackWidget) => void; - -/** - * Optional callback function for custom widget content rendering. - * Called during load()/addWidget() to create custom content beyond plain text. - * - * @param el - The widget's content container element - * @param w - The widget definition with content and other properties - */ -export type RenderFcn = (el: HTMLElement, w: GridStackWidget) => void; - -/** - * Optional callback function for custom resize-to-content behavior. - * Called when a widget needs to resize to fit its content. - * - * @param el - The grid item element to resize - */ -export type ResizeToContentFcn = (el: GridItemHTMLElement) => void; - -/** - * Configuration for responsive grid behavior. - * - * Defines how the grid responds to different screen sizes by changing column counts. - * NOTE: Make sure to include the appropriate CSS (gridstack-extra.css) to support responsive behavior. - */ -export interface Responsive { - /** wanted width to maintain (+-50%) to dynamically pick a column count. NOTE: make sure to have correct extra CSS to support this. */ - columnWidth?: number; - /** maximum number of columns allowed (default: 12). NOTE: make sure to have correct extra CSS to support this. */ - columnMax?: number; - /** explicit width:column breakpoints instead of automatic 'columnWidth'. NOTE: make sure to have correct extra CSS to support this. */ - breakpoints?: Breakpoint[]; - /** specify if breakpoints are for window size or grid size (default:false = grid) */ - breakpointForWindow?: boolean; - /** global re-layout mode when changing columns */ - layout?: ColumnOptions; -} - -/** - * Defines a responsive breakpoint for automatic column count changes. - * Used with the responsive.breakpoints option. - */ -export interface Breakpoint { - /** Maximum width (in pixels) for this breakpoint to be active */ - w?: number; - /** Number of columns to use when this breakpoint is active */ - c: number; - /** Layout mode for this specific breakpoint (overrides global responsive.layout) */ - layout?: ColumnOptions; - /** TODO: Future feature - specific children layout for this breakpoint */ - // children?: GridStackWidget[]; -} - -/** - * Defines the options for a Grid - */ -export interface GridStackOptions { - /** - * Accept widgets dragged from other grids or from outside (default: `false`). Can be: - * - `true`: will accept HTML elements having 'grid-stack-item' as class attribute - * - `false`: will not accept any external widgets - * - string: explicit class name to accept instead of default - * - function: callback called before an item will be accepted when entering a grid - * - * @example - * // Accept all grid items - * acceptWidgets: true - * - * // Accept only items with specific class - * acceptWidgets: 'my-draggable-item' - * - * // Custom validation function - * acceptWidgets: (el) => { - * return el.getAttribute('data-accept') === 'true'; - * } - * - * @see {@link http://gridstack.github.io/gridstack.js/demo/two.html} for complete example - */ - acceptWidgets?: boolean | string | ((element: Element) => boolean); - - /** possible values (default: `mobile`) - does not apply to non-resizable widgets - * `false` the resizing handles are only shown while hovering over a widget - * `true` the resizing handles are always shown - * 'mobile' if running on a mobile device, default to `true` (since there is no hovering per say), else `false`. - See [example](http://gridstack.github.io/gridstack.js/demo/mobile.html) */ - alwaysShowResizeHandle?: true | false | 'mobile'; - - /** turns animation on (default?: true) */ - animate?: boolean; - - /** if false gridstack will not initialize existing items (default?: true) */ - auto?: boolean; - - /** - * One cell height (default: 'auto'). Can be: - * - an integer (px): fixed pixel height - * - a string (ex: '100px', '10em', '10rem'): CSS length value - * - 0: library will not generate styles for rows (define your own CSS) - * - 'auto': height calculated for square cells (width / column) and updated live on window resize - * - 'initial': similar to 'auto' but stays fixed size during window resizing - * - * Note: % values don't work correctly - see demo/cell-height.html - * - * @example - * // Fixed 100px height - * cellHeight: 100 - * - * // CSS units - * cellHeight: '5rem' - * cellHeight: '100px' - * - * // Auto-sizing for square cells - * cellHeight: 'auto' - * - * // No CSS generation (custom styles) - * cellHeight: 0 - */ - cellHeight?: numberOrString; - - /** throttle time delay (in ms) used when cellHeight='auto' to improve performance vs usability (default?: 100). - * A value of 0 will make it instant at a cost of re-creating the CSS file at ever window resize event! - * */ - cellHeightThrottle?: number; - - /** (internal) unit for cellHeight (default? 'px') which is set when a string cellHeight with a unit is passed (ex: '10rem') */ - cellHeightUnit?: string; - - /** list of children item to create when calling load() or addGrid() */ - children?: GridStackWidget[]; - - /** number of columns (default?: 12). Note: IF you change this, CSS also have to change. See https://github.com/gridstack/gridstack.js#change-grid-columns. - * Note: for nested grids, it is recommended to use 'auto' which will always match the container grid-item current width (in column) to keep inside and outside - * items always the same. flag is NOT supported for regular non-nested grids. - */ - column?: number | 'auto'; - - /** responsive column layout for width:column behavior */ - columnOpts?: Responsive; - - /** additional class on top of '.grid-stack' (which is required for our CSS) to differentiate this instance. - Note: only used by addGrid(), else your element should have the needed class */ - class?: string; - - /** disallows dragging of widgets (default?: false) */ - disableDrag?: boolean; - - /** disallows resizing of widgets (default?: false). */ - disableResize?: boolean; - - /** allows to override UI draggable options. (default?: { handle?: '.grid-stack-item-content', appendTo?: 'body' }) */ - draggable?: DDDragOpt; - - /** let user drag nested grid items out of a parent or not (default true - not supported yet) */ - //dragOut?: boolean; - - /** the type of engine to create (so you can subclass) default to GridStackEngine */ - engineClass?: typeof GridStackEngine; - - /** enable floating widgets (default?: false) See example (http://gridstack.github.io/gridstack.js/demo/float.html) */ - float?: boolean; - - /** draggable handle selector (default?: '.grid-stack-item-content') */ - handle?: string; - - /** draggable handle class (e.g. 'grid-stack-item-content'). If set 'handle' is ignored (default?: null) */ - handleClass?: string; - - /** additional widget class (default?: 'grid-stack-item') */ - itemClass?: string; - - /** re-layout mode when we're a subgrid and we are being resized. default to 'list' */ - layout?: ColumnOptions; - - /** true when widgets are only created when they scroll into view (visible) */ - lazyLoad?: boolean; - - /** - * gap between grid item and content (default?: 10). This will set all 4 sides and support the CSS formats below - * an integer (px) - * a string with possible units (ex: '2em', '20px', '2rem') - * string with space separated values (ex: '5px 10px 0 20px' for all 4 sides, or '5em 10em' for top/bottom and left/right pairs like CSS). - * Note: all sides must have same units (last one wins, default px) - */ - margin?: numberOrString; - - /** OLD way to optionally set each side - use margin: '5px 10px 0 20px' instead. Used internally to store each side. */ - marginTop?: numberOrString; - marginRight?: numberOrString; - marginBottom?: numberOrString; - marginLeft?: numberOrString; - - /** (internal) unit for margin (default? 'px') set when `margin` is set as string with unit (ex: 2rem') */ - marginUnit?: string; - - /** maximum rows amount. Default? is 0 which means no maximum rows */ - maxRow?: number; - - /** minimum rows amount which is handy to prevent grid from collapsing when empty. Default is `0`. - * When no set the `min-height` CSS attribute on the grid div (in pixels) can be used, which will round to the closest row. - */ - minRow?: number; - - /** If you are using a nonce-based Content Security Policy, pass your nonce here and - * GridStack will add it to the