diff --git a/spec/gridstack-spec.ts b/spec/gridstack-spec.ts index 89289795f..30e064591 100644 --- a/spec/gridstack-spec.ts +++ b/spec/gridstack-spec.ts @@ -246,20 +246,7 @@ describe('gridstack', function() { expect(grid.getColumn()).toBe(12); grid.column(12); expect(grid.getColumn()).toBe(12); - }); - it('should set construct CSS class', function() { - let grid = GridStack.init({column: 1}); - expect(grid.el.classList.contains('grid-stack-1')).toBe(true); - grid.column(2); - expect(grid.el.classList.contains('grid-stack-1')).toBe(false); - expect(grid.el.classList.contains('grid-stack-2')).toBe(true); - }); - it('should set CSS class', function() { - let grid = GridStack.init(); - expect(grid.el.classList.contains('grid-stack')).toBe(true); - grid.column(1); - expect(grid.el.classList.contains('grid-stack-1')).toBe(true); - }); + }); it('should SMALL change column number, no relayout', function() { let options = { column: 12 @@ -1362,51 +1349,7 @@ describe('gridstack', function() { expect(grid.el.classList.contains('grid-stack-rtl')).toBe(false); }); }); - - describe('grid.opts.styleInHead', function() { - beforeEach(function() { - document.body.insertAdjacentHTML('afterbegin', gridstackHTML); - }); - afterEach(function() { - document.body.removeChild(document.getElementById('gs-cont')); - }); - it('should add STYLE to parent node as a default', function() { - var options = { - cellHeight: 80, - verticalMargin: 10, - float: false, - }; - var grid = GridStack.init(options); - expect((grid as any)._styles.ownerNode.parentNode.tagName).toBe('DIV'); // any to access private _styles - }); - it('should add STYLE to HEAD if styleInHead === true', function() { - var options = { - cellHeight: 80, - verticalMargin: 10, - float: false, - styleInHead: true - }; - var grid = GridStack.init(options); - expect((grid as any)._styles.ownerNode.parentNode.tagName).toBe('HEAD'); // any to access private _styles - }); - }); - describe('grid.opts.styleInHead', function() { - beforeEach(function() { - document.body.insertAdjacentHTML('afterbegin', gridstackHTML); - }); - afterEach(function() { - document.body.removeChild(document.getElementById('gs-cont')); - }); - it('should add STYLE to parent node as a default', function() { - var grid = GridStack.init(); - expect((grid as any)._styles.ownerNode.parentNode.tagName).toBe('DIV'); - }); - it('should add STYLE to HEAD if styleInHead === true', function() { - var grid = GridStack.init({styleInHead: true}); - expect((grid as any)._styles.ownerNode.parentNode.tagName).toBe('HEAD'); - }); - }); describe('grid.enableMove', function() { beforeEach(function() { diff --git a/spec/utils-spec.ts b/spec/utils-spec.ts index 04dc18e84..ae5772dbe 100644 --- a/spec/utils-spec.ts +++ b/spec/utils-spec.ts @@ -48,22 +48,6 @@ describe('gridstack utils', function() { }); }); - describe('test createStylesheet/removeStylesheet', function() { - - it('should create/remove style DOM', function() { - let _id = 'test-123'; - Utils.createStylesheet(_id); - - let style = document.querySelector('STYLE[gs-style-id=' + _id + ']'); - expect(style).not.toBe(null); - // expect(style.prop('tagName')).toEqual('STYLE'); - - Utils.removeStylesheet(_id) - style = document.querySelector('STYLE[gs-style-id=' + _id + ']'); - expect(style).toBe(null); - }); - - }); describe('test parseHeight', function() { diff --git a/src/gridstack-dd.ts b/src/gridstack-dd.ts index 515f3d0c5..2f0771573 100644 --- a/src/gridstack-dd.ts +++ b/src/gridstack-dd.ts @@ -292,6 +292,9 @@ GridStack.prototype._setupAcceptWidget = function(this: GridStack): GridStack { } else { this.engine.removeNode(node); } + if(!Utils.isConstructableStyleSheetSupported()) { + this._updateElementChildrenStyling(node.el); + } }); return false; // prevent parent from receiving msg (which may be grid as well) diff --git a/src/gridstack.ts b/src/gridstack.ts index 541d49b0b..964c31448 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -229,6 +229,8 @@ export class GridStack { public _gsEventHandler = {}; /** @internal */ protected _styles: GridCSSStyleSheet; + /** @internal max row index of the gridstack*/ + private _maxRowIndex: number; /** @internal flag to keep cells square during resize */ protected _isAutoCellHeight: boolean; /** @internal track event binding to window resize so we can remove */ @@ -476,6 +478,10 @@ export class GridStack { this._triggerAddEvent(); this._triggerChangeEvent(); + if(!Utils.isConstructableStyleSheetSupported()) { + Utils.updatePositionStyleOnWidget(el, this.opts.cellHeight as number, this.opts.cellHeightUnit); + this._updateElementChildrenStyling(el); + } return el; } @@ -1180,16 +1186,18 @@ export class GridStack { protected _removeStylesheet(): GridStack { if (this._styles) { - Utils.removeStylesheet(this._styles._id); + Utils.removeStylesheet(this._styles); delete this._styles; } return this; } - /** @internal updated/create the CSS styles for row based layout and initial margin setting */ + /** @internal updated/create the CSS styles for row based layout and initial margin setting + * in case no constructable stylesheet support updated/create the CSS styles on elements for row based layout and initial margin setting */ protected _updateStyles(forceUpdate = false, maxH?: number): GridStack { // call to delete existing one if we change cellHeight / margin if (forceUpdate) { + this._maxRowIndex = undefined; this._removeStylesheet(); } @@ -1204,12 +1212,56 @@ export class GridStack { let cellHeightUnit = this.opts.cellHeightUnit; let prefix = `.${this.opts._styleSheetClass} > .${this.opts.itemClass}`; + if(Utils.isConstructableStyleSheetSupported()) { + return this._updateStyleRules(cellHeight, cellHeightUnit, prefix, maxH); + } else { + return this._updatedStylesForElements(cellHeight, cellHeightUnit, prefix, maxH); + } + } + + /** @internal update Styling */ + protected _updateElementChildrenStyling(el?: HTMLElement, prefix?: string): void { + let top: string = this.opts.marginTop + this.opts.marginUnit; + let bottom: string = this.opts.marginBottom + this.opts.marginUnit; + let right: string = this.opts.marginRight + this.opts.marginUnit; + let left: string = this.opts.marginLeft + this.opts.marginUnit; + let content: string; + let placeholder = `.${this.opts._styleSheetClass} > .grid-stack-placeholder > .placeholder-content`; + if (el!==undefined) { + content = `.grid-stack-item-content` + // content margins + Utils.updateStyleOnElements([el.querySelector(content) as HTMLElement], {top, right, bottom, left}); + Utils.updateStyleOnElements(placeholder, {top, right, bottom, left}); + // resize handles offset (to match margin) + Utils.updateStyleOnElements([el.querySelector(`.ui-resizable-ne`) as HTMLElement], {right}); + Utils.updateStyleOnElements([el.querySelector(`.ui-resizable-e`) as HTMLElement], {right}); + Utils.updateStyleOnElements([el.querySelector(`.ui-resizable-se`) as HTMLElement], {right, bottom}); + Utils.updateStyleOnElements([el.querySelector(`.ui-resizable-nw`) as HTMLElement], {left}); + Utils.updateStyleOnElements([el.querySelector(`.ui-resizable-w`) as HTMLElement], {left}); + Utils.updateStyleOnElements([el.querySelector(`.ui-resizable-sw`) as HTMLElement], {left, bottom}); + } else if (prefix!= undefined) { + content = `${prefix} > .grid-stack-item-content`; + // content margins + Utils.updateStyleOnElements(content, {top, right, bottom, left}); + Utils.updateStyleOnElements(placeholder, {top, right, bottom, left}); + // resize handles offset (to match margin) + Utils.updateStyleOnElements(`${prefix} > .ui-resizable-ne`, {right}); + Utils.updateStyleOnElements(`${prefix} > .ui-resizable-e`, {right}); + Utils.updateStyleOnElements(`${prefix} > .ui-resizable-se`, {right, bottom}); + Utils.updateStyleOnElements(`${prefix} > .ui-resizable-nw`, {left}); + Utils.updateStyleOnElements(`${prefix} > .ui-resizable-w`, {left}); + Utils.updateStyleOnElements(`${prefix} > .ui-resizable-sw`, {left, bottom}); + } + } + + /**@internal */ + protected _updateStyleRules(cellHeight:number, cellHeightUnit:string, prefix:string, maxH?: number): GridStack { // create one as needed if (!this._styles) { let id = 'gridstack-style-' + (Math.random() * 100000).toFixed(); // insert style to parent (instead of 'head' by default) to support WebComponent let styleLocation = this.opts.styleInHead ? undefined : this.el.parentNode as HTMLElement; - this._styles = Utils.createStylesheet(id, styleLocation); + this._styles = Utils.createStylesheet(styleLocation); if (!this._styles) return this; this._styles._id = id; this._styles._max = 0; @@ -1250,6 +1302,32 @@ export class GridStack { return this; } + /**@internal updates styles for dirty nodes */ + protected _updatedStylesForElements(cellHeight:number, cellHeightUnit:string, prefix:string, maxH?: number): GridStack { + // create one as needed + if (!this._maxRowIndex) { + this._maxRowIndex = 0; + + // these are done once only + Utils.updateStyleOnElements(prefix, {'min-height': cellHeight+cellHeightUnit}); + this._updateElementChildrenStyling(undefined, prefix); + } + + // apply position styling on all dirty node's elements + this.getGridItems().forEach((w: GridItemHTMLElement) => { + if (w.gridstackNode._dirty && !w.classList.contains("ui-resizable-resizing")) { + Utils.updatePositionStyleOnWidget(w, this.opts.cellHeight as number, this.opts.cellHeightUnit); + } + }); + + // now update the height specific fields + maxH = maxH || this._maxRowIndex; + if (maxH > this._maxRowIndex) { + this._maxRowIndex = maxH; + } + return this; + } + /** @internal */ protected _updateContainerHeight(): GridStack { if (!this.engine || this.engine.batchMode) return this; @@ -1301,6 +1379,10 @@ export class GridStack { if (n.y !== undefined && n.y !== null) { el.setAttribute('gs-y', String(n.y)); } if (n.w) { el.setAttribute('gs-w', String(n.w)); } if (n.h) { el.setAttribute('gs-h', String(n.h)); } + if(!Utils.isConstructableStyleSheetSupported()) { + Utils.updatePositionStyleOnWidget(el, this.opts.cellHeight as number, this.opts.cellHeightUnit); + this._updateElementChildrenStyling(el); + } return this; } diff --git a/src/h5/dd-draggable.ts b/src/h5/dd-draggable.ts index 00689ca49..7eb9d57ab 100644 --- a/src/h5/dd-draggable.ts +++ b/src/h5/dd-draggable.ts @@ -10,350 +10,350 @@ import { GridItemHTMLElement, DDUIData } from '../types'; // TODO: merge with DDDragOpt ? export interface DDDraggableOpt { - appendTo?: string | HTMLElement; - containment?: string | HTMLElement; // TODO: not implemented yet - handle?: string; - revert?: string | boolean | unknown; // TODO: not implemented yet - scroll?: boolean; // nature support by HTML5 drag drop, can't be switch to off actually - helper?: string | HTMLElement | ((event: Event) => HTMLElement); - start?: (event: Event, ui: DDUIData) => void; - stop?: (event: Event) => void; - drag?: (event: Event, ui: DDUIData) => void; -} + appendTo?: string | HTMLElement; + containment?: string | HTMLElement; // TODO: not implemented yet + handle?: string; + revert?: string | boolean | unknown; // TODO: not implemented yet + scroll?: boolean; // nature support by HTML5 drag drop, can't be switch to off actually + helper?: string | HTMLElement | ((event: Event) => HTMLElement); + start?: (event: Event, ui: DDUIData) => void; + stop?: (event: Event) => void; + drag?: (event: Event, ui: DDUIData) => void; + } -interface DragOffset { - left: number; - top: number; - width: number; - height: number; - offsetLeft: number; - offsetTop: number; -} + interface DragOffset { + left: number; + top: number; + width: number; + height: number; + offsetLeft: number; + offsetTop: number; + } export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt { - public el: HTMLElement; - public option: DDDraggableOpt; - public helper: HTMLElement; // used by GridStackDDNative + public el: HTMLElement; + public option: DDDraggableOpt; + public helper: HTMLElement; // used by GridStackDDNative - /** @internal */ - protected dragOffset: DragOffset; - /** @internal */ - protected dragElementOriginStyle: Array; - /** @internal */ - protected dragFollowTimer: number; - /** @internal */ - protected dragEl: HTMLElement; - /** @internal */ - protected dragging = false; - /** @internal */ - protected paintTimer: number; - /** @internal */ - protected parentOriginStylePosition: string; - /** @internal */ - protected helperContainment: HTMLElement; - /** @internal #1541 can't have {passive: true} on Safari as otherwise it reverts animate back to old location on drop */ - protected static dragEventListenerOption = true; // DDUtils.isEventSupportPassiveOption ? { capture: true, passive: true } : true; - /** @internal properties we change during dragging, and restore back */ - protected static originStyleProp = ['transition', 'pointerEvents', 'position', - 'left', 'top', 'opacity', 'zIndex', 'width', 'height', 'willChange', 'min-width']; + /** @internal */ + protected dragOffset: DragOffset; + /** @internal */ + protected dragElementOriginStyle: Array; + /** @internal */ + protected dragFollowTimer: number; + /** @internal */ + protected dragEl: HTMLElement; + /** @internal */ + protected dragging = false; + /** @internal */ + protected paintTimer: number; + /** @internal */ + protected parentOriginStylePosition: string; + /** @internal */ + protected helperContainment: HTMLElement; + /** @internal #1541 can't have {passive: true} on Safari as otherwise it reverts animate back to old location on drop */ + protected static dragEventListenerOption = true; // DDUtils.isEventSupportPassiveOption ? { capture: true, passive: true } : true; + /** @internal properties we change during dragging, and restore back */ + protected static originStyleProp = ['transition', 'pointerEvents', 'position', + 'left', 'top', 'opacity', 'zIndex', 'width', 'height', 'willChange', 'min-width']; - constructor(el: HTMLElement, option: DDDraggableOpt = {}) { - super(); - this.el = el; - this.option = option; - // get the element that is actually supposed to be dragged by - let className = option.handle.substring(1); - this.dragEl = el.classList.contains(className) ? el : el.querySelector(option.handle) || el; - // create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions) - this._dragStart = this._dragStart.bind(this); - this._drag = this._drag.bind(this); - this._dragEnd = this._dragEnd.bind(this); - this.enable(); - } + constructor(el: HTMLElement, option: DDDraggableOpt = {}) { + super(); + this.el = el; + this.option = option; + // get the element that is actually supposed to be dragged by + let className = option.handle.substring(1); + this.dragEl = el.classList.contains(className) ? el : el.querySelector(option.handle) || el; + // create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions) + this._dragStart = this._dragStart.bind(this); + this._drag = this._drag.bind(this); + this._dragEnd = this._dragEnd.bind(this); + this.enable(); + } - public on(event: 'drag' | 'dragstart' | 'dragstop', callback: (event: DragEvent) => void): void { - super.on(event, callback); - } + public on(event: 'drag' | 'dragstart' | 'dragstop', callback: (event: DragEvent) => void): void { + super.on(event, callback); + } - public off(event: 'drag' | 'dragstart' | 'dragstop'): void { - super.off(event); - } + public off(event: 'drag' | 'dragstart' | 'dragstop'): void { + super.off(event); + } - public enable(): void { - super.enable(); - this.dragEl.draggable = true; - this.dragEl.addEventListener('dragstart', this._dragStart); - this.el.classList.remove('ui-draggable-disabled'); - this.el.classList.add('ui-draggable'); - } + public enable(): void { + super.enable(); + this.dragEl.draggable = true; + this.dragEl.addEventListener('dragstart', this._dragStart); + this.el.classList.remove('ui-draggable-disabled'); + this.el.classList.add('ui-draggable'); + } - public disable(forDestroy = false): void { - super.disable(); - this.dragEl.removeAttribute('draggable'); - this.dragEl.removeEventListener('dragstart', this._dragStart); - this.el.classList.remove('ui-draggable'); - if (!forDestroy) this.el.classList.add('ui-draggable-disabled'); - } + public disable(forDestroy = false): void { + super.disable(); + this.dragEl.removeAttribute('draggable'); + this.dragEl.removeEventListener('dragstart', this._dragStart); + this.el.classList.remove('ui-draggable'); + if (!forDestroy) this.el.classList.add('ui-draggable-disabled'); + } - public destroy(): void { - if (this.dragging) { - // Destroy while dragging should remove dragend listener and manually trigger - // dragend, otherwise dragEnd can't perform dragstop because eventRegistry is - // destroyed. - this._dragEnd({} as DragEvent); - } - this.disable(true); - delete this.el; - delete this.helper; - delete this.option; - super.destroy(); - } + public destroy(): void { + if (this.dragging) { + // Destroy while dragging should remove dragend listener and manually trigger + // dragend, otherwise dragEnd can't perform dragstop because eventRegistry is + // destroyed. + this._dragEnd({} as DragEvent); + } + this.disable(true); + delete this.el; + delete this.helper; + delete this.option; + super.destroy(); + } - public updateOption(opts: DDDraggableOpt): DDDraggable { - Object.keys(opts).forEach(key => this.option[key] = opts[key]); - return this; - } + public updateOption(opts: DDDraggableOpt): DDDraggable { + Object.keys(opts).forEach(key => this.option[key] = opts[key]); + return this; + } - /** @internal */ - protected _dragStart(event: DragEvent): void { - DDManager.dragElement = this; - this.helper = this._createHelper(event); - this._setupHelperContainmentStyle(); - this.dragOffset = this._getDragOffset(event, this.el, this.helperContainment); - const ev = DDUtils.initEvent(event, { target: this.el, type: 'dragstart' }); - if (this.helper !== this.el) { - this._setupDragFollowNodeNotifyStart(ev); - // immediately set external helper initial position to avoid flickering behavior and unnecessary looping in `_packNodes()` - this._dragFollow(event); - } else { - this.dragFollowTimer = window.setTimeout(() => { - delete this.dragFollowTimer; - this._setupDragFollowNodeNotifyStart(ev); - }, 0); - } - this._cancelDragGhost(event); - } + /** @internal */ + protected _dragStart(event: DragEvent): void { + DDManager.dragElement = this; + this.helper = this._createHelper(event); + this._setupHelperContainmentStyle(); + this.dragOffset = this._getDragOffset(event, this.el, this.helperContainment); + const ev = DDUtils.initEvent(event, { target: this.el, type: 'dragstart' }); + if (this.helper !== this.el) { + this._setupDragFollowNodeNotifyStart(ev); + // immediately set external helper initial position to avoid flickering behavior and unnecessary looping in `_packNodes()` + this._dragFollow(event); + } else { + this.dragFollowTimer = window.setTimeout(() => { + delete this.dragFollowTimer; + this._setupDragFollowNodeNotifyStart(ev); + }, 0); + } + this._cancelDragGhost(event); + } - /** @internal */ - protected _setupDragFollowNodeNotifyStart(ev: Event): DDDraggable { - this._setupHelperStyle(); - document.addEventListener('dragover', this._drag, DDDraggable.dragEventListenerOption); - this.dragEl.addEventListener('dragend', this._dragEnd); - if (this.option.start) { - this.option.start(ev, this.ui()); - } - this.dragging = true; - this.helper.classList.add('ui-draggable-dragging'); - this.triggerEvent('dragstart', ev); - return this; - } + /** @internal */ + protected _setupDragFollowNodeNotifyStart(ev: Event): DDDraggable { + this._setupHelperStyle(); + document.addEventListener('dragover', this._drag, DDDraggable.dragEventListenerOption); + this.dragEl.addEventListener('dragend', this._dragEnd); + if (this.option.start) { + this.option.start(ev, this.ui()); + } + this.dragging = true; + this.helper.classList.add('ui-draggable-dragging'); + this.triggerEvent('dragstart', ev); + return this; + } - /** @internal */ - protected _drag(event: DragEvent): void { - // Safari: prevent default to allow drop to happen instead of reverting back (with animation) and delaying dragend #1541 - // https://stackoverflow.com/questions/61760755/how-to-fire-dragend-event-immediately - event.preventDefault(); - this._dragFollow(event); - const ev = DDUtils.initEvent(event, { target: this.el, type: 'drag' }); - if (this.option.drag) { - this.option.drag(ev, this.ui()); - } - this.triggerEvent('drag', ev); - } + /** @internal */ + protected _drag(event: DragEvent): void { + // Safari: prevent default to allow drop to happen instead of reverting back (with animation) and delaying dragend #1541 + // https://stackoverflow.com/questions/61760755/how-to-fire-dragend-event-immediately + event.preventDefault(); + this._dragFollow(event); + const ev = DDUtils.initEvent(event, { target: this.el, type: 'drag' }); + if (this.option.drag) { + this.option.drag(ev, this.ui()); + } + this.triggerEvent('drag', ev); + } - /** @internal */ - protected _dragEnd(event: DragEvent): void { - if (this.dragFollowTimer) { - clearTimeout(this.dragFollowTimer); - delete this.dragFollowTimer; - return; - } else { - if (this.paintTimer) { - cancelAnimationFrame(this.paintTimer); - } - document.removeEventListener('dragover', this._drag, DDDraggable.dragEventListenerOption); - this.dragEl.removeEventListener('dragend', this._dragEnd); - } - this.dragging = false; - this.helper.classList.remove('ui-draggable-dragging'); - this.helperContainment.style.position = this.parentOriginStylePosition || null; - if (this.helper === this.el) { - this._removeHelperStyle(); - } else { - this.helper.remove(); - } - const ev = DDUtils.initEvent(event, { target: this.el, type: 'dragstop' }); - if (this.option.stop) { - this.option.stop(ev); // Note: ui() not used by gridstack so don't pass - } - this.triggerEvent('dragstop', ev); - delete DDManager.dragElement; - delete this.helper; - } + /** @internal */ + protected _dragEnd(event: DragEvent): void { + if (this.dragFollowTimer) { + clearTimeout(this.dragFollowTimer); + delete this.dragFollowTimer; + return; + } else { + if (this.paintTimer) { + cancelAnimationFrame(this.paintTimer); + } + document.removeEventListener('dragover', this._drag, DDDraggable.dragEventListenerOption); + this.dragEl.removeEventListener('dragend', this._dragEnd); + } + this.dragging = false; + this.helper.classList.remove('ui-draggable-dragging'); + this.helperContainment.style.position = this.parentOriginStylePosition || null; + if (this.helper === this.el) { + this._removeHelperStyle(); + } else { + this.helper.remove(); + } + const ev = DDUtils.initEvent(event, { target: this.el, type: 'dragstop' }); + if (this.option.stop) { + this.option.stop(ev); // Note: ui() not used by gridstack so don't pass + } + this.triggerEvent('dragstop', ev); + delete DDManager.dragElement; + delete this.helper; + } - /** @internal create a clone copy (or user defined method) of the original drag item if set */ - protected _createHelper(event: DragEvent): HTMLElement { - let helper = this.el; - if (typeof this.option.helper === 'function') { - helper = this.option.helper(event); - } else if (this.option.helper === 'clone') { - helper = DDUtils.clone(this.el); - } - if (!document.body.contains(helper)) { - DDUtils.appendTo(helper, this.option.appendTo === 'parent' ? this.el.parentNode : this.option.appendTo); - } - if (helper === this.el) { - this.dragElementOriginStyle = DDDraggable.originStyleProp.map(prop => this.el.style[prop]); - } - return helper; - } + /** @internal create a clone copy (or user defined method) of the original drag item if set */ + protected _createHelper(event: DragEvent): HTMLElement { + let helper = this.el; + if (typeof this.option.helper === 'function') { + helper = this.option.helper(event); + } else if (this.option.helper === 'clone') { + helper = DDUtils.clone(this.el); + } + if (!document.body.contains(helper)) { + DDUtils.appendTo(helper, this.option.appendTo === 'parent' ? this.el.parentNode : this.option.appendTo); + } + if (helper === this.el) { + this.dragElementOriginStyle = DDDraggable.originStyleProp.map(prop => this.el.style[prop]); + } + return helper; + } - /** @internal */ - protected _setupHelperStyle(): DDDraggable { - // TODO: set all at once with style.cssText += ... ? https://stackoverflow.com/questions/3968593 - const rec = this.helper.getBoundingClientRect(); - const style = this.helper.style; - style.pointerEvents = 'none'; - style['min-width'] = 0; // since we no longer relative to our parent and we don't resize anyway (normally 100/#column %) - style.width = this.dragOffset.width + 'px'; - style.height = this.dragOffset.height + 'px'; - style.willChange = 'left, top'; - style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative' - style.left = rec.left + 'px'; - style.top = rec.top + 'px'; - style.transition = 'none'; // show up instantly - setTimeout(() => { - if (this.helper) { - style.transition = null; // recover animation - } - }, 0); - return this; - } + /** @internal */ + protected _setupHelperStyle(): DDDraggable { + // TODO: set all at once with style.cssText += ... ? https://stackoverflow.com/questions/3968593 + const rec = this.helper.getBoundingClientRect(); + const style = this.helper.style; + style.pointerEvents = 'none'; + style['min-width'] = 0; // since we no longer relative to our parent and we don't resize anyway (normally 100/#column %) + style.width = this.dragOffset.width + 'px'; + style.height = this.dragOffset.height + 'px'; + style.willChange = 'left, top'; + style.position = 'fixed'; // let us drag between grids by not clipping as parent .grid-stack is position: 'relative' + style.left = rec.left + 'px'; + style.top = rec.top + 'px'; + style.transition = 'none'; // show up instantly + setTimeout(() => { + if (this.helper) { + style.transition = null; // recover animation + } + }, 0); + return this; + } - /** @internal */ - protected _removeHelperStyle(): DDDraggable { - let node = (this.helper as GridItemHTMLElement)?.gridstackNode; - // don't bother restoring styles if we're gonna remove anyway... - if (this.dragElementOriginStyle && (!node || !node._isAboutToRemove)) { - let helper = this.helper; - // don't animate, otherwise we animate offseted when switching back to 'absolute' from 'fixed' - let transition = this.dragElementOriginStyle['transition'] || null; - helper.style.transition = this.dragElementOriginStyle['transition'] = 'none'; - DDDraggable.originStyleProp.forEach(prop => helper.style[prop] = this.dragElementOriginStyle[prop] || null); - setTimeout(() => helper.style.transition = transition, 50); // recover animation from saved vars after a pause (0 isn't enough #1973) - } - delete this.dragElementOriginStyle; - return this; - } + /** @internal */ + protected _removeHelperStyle(): DDDraggable { + let node = (this.helper as GridItemHTMLElement)?.gridstackNode; + // don't bother restoring styles if we're gonna remove anyway... + if (this.dragElementOriginStyle && (!node || !node._isAboutToRemove)) { + let helper = this.helper; + // don't animate, otherwise we animate offseted when switching back to 'absolute' from 'fixed' + let transition = this.dragElementOriginStyle['transition'] || null; + helper.style.transition = this.dragElementOriginStyle['transition'] = 'none'; + DDDraggable.originStyleProp.forEach(prop => helper.style[prop] = this.dragElementOriginStyle[prop] || null); + setTimeout(() => helper.style.transition = transition, 50); // recover animation from saved vars after a pause (0 isn't enough #1973) + } + delete this.dragElementOriginStyle; + return this; + } - /** @internal */ - protected _dragFollow(event: DragEvent): void { - if (this.paintTimer) { - cancelAnimationFrame(this.paintTimer); - } - this.paintTimer = requestAnimationFrame(() => { - delete this.paintTimer; - const offset = this.dragOffset; - let containmentRect = { left: 0, top: 0 }; - if (this.helper.style.position === 'absolute') { - const { left, top } = this.helperContainment.getBoundingClientRect(); - containmentRect = { left, top }; - } - this.helper.style.left = event.clientX + offset.offsetLeft - containmentRect.left + 'px'; - this.helper.style.top = event.clientY + offset.offsetTop - containmentRect.top + 'px'; - }); - } + /** @internal */ + protected _dragFollow(event: DragEvent): void { + if (this.paintTimer) { + cancelAnimationFrame(this.paintTimer); + } + this.paintTimer = requestAnimationFrame(() => { + delete this.paintTimer; + const offset = this.dragOffset; + let containmentRect = { left: 0, top: 0 }; + if (this.helper.style.position === 'absolute') { + const { left, top } = this.helperContainment.getBoundingClientRect(); + containmentRect = { left, top }; + } + this.helper.style.left = event.clientX + offset.offsetLeft - containmentRect.left + 'px'; + this.helper.style.top = event.clientY + offset.offsetTop - containmentRect.top + 'px'; + }); + } - /** @internal */ - protected _setupHelperContainmentStyle(): DDDraggable { - this.helperContainment = this.helper.parentElement; - if (this.helper.style.position !== 'fixed') { - this.parentOriginStylePosition = this.helperContainment.style.position; - if (window.getComputedStyle(this.helperContainment).position.match(/static/)) { - this.helperContainment.style.position = 'relative'; - } - } - return this; - } + /** @internal */ + protected _setupHelperContainmentStyle(): DDDraggable { + this.helperContainment = this.helper.parentElement; + if (this.helper.style.position !== 'fixed') { + this.parentOriginStylePosition = this.helperContainment.style.position; + if (window.getComputedStyle(this.helperContainment).position.match(/static/)) { + this.helperContainment.style.position = 'relative'; + } + } + return this; + } - /** @internal prevent the default ghost image to be created (which has wrong as we move the helper/element instead - * (legacy jquery UI code updates the top/left of the item). - * TODO: maybe use mouse event instead of HTML5 drag as we have to work around it anyway, or change code to not update - * the actual grid-item but move the ghost image around (and special case jq version) ? - **/ - protected _cancelDragGhost(e: DragEvent): DDDraggable { - /* doesn't seem to do anything... - let t = e.dataTransfer; - t.effectAllowed = 'none'; - t.dropEffect = 'none'; - t.setData('text', ''); - */ + /** @internal prevent the default ghost image to be created (which has wrong as we move the helper/element instead + * (legacy jquery UI code updates the top/left of the item). + * TODO: maybe use mouse event instead of HTML5 drag as we have to work around it anyway, or change code to not update + * the actual grid-item but move the ghost image around (and special case jq version) ? + **/ + protected _cancelDragGhost(e: DragEvent): DDDraggable { + /* doesn't seem to do anything... + let t = e.dataTransfer; + t.effectAllowed = 'none'; + t.dropEffect = 'none'; + t.setData('text', ''); + */ - // NOTE: according to spec (and required by Safari see #1540) the image has to be visible in the browser (in dom and not hidden) so make it a 1px div - let img = document.createElement('div'); - img.style.width = '1px'; - img.style.height = '1px'; - img.style.position = 'fixed'; // prevent unwanted scrollbar - document.body.appendChild(img); - e.dataTransfer.setDragImage(img, 0, 0); - setTimeout(() => document.body.removeChild(img)); // nuke once drag had a chance to grab this 'image' + // NOTE: according to spec (and required by Safari see #1540) the image has to be visible in the browser (in dom and not hidden) so make it a 1px div + let img = document.createElement('div'); + img.style.width = '1px'; + img.style.height = '1px'; + img.style.position = 'fixed'; // prevent unwanted scrollbar + document.body.appendChild(img); + e.dataTransfer.setDragImage(img, 0, 0); + setTimeout(() => document.body.removeChild(img)); // nuke once drag had a chance to grab this 'image' - e.stopPropagation(); - return this; - } + e.stopPropagation(); + return this; + } - /** @internal */ - protected _getDragOffset(event: DragEvent, el: HTMLElement, parent: HTMLElement): DragOffset { + /** @internal */ + protected _getDragOffset(event: DragEvent, el: HTMLElement, parent: HTMLElement): DragOffset { - // in case ancestor has transform/perspective css properties that change the viewpoint - let xformOffsetX = 0; - let xformOffsetY = 0; - if (parent) { - const testEl = document.createElement('div'); - DDUtils.addElStyles(testEl, { - opacity: '0', - position: 'fixed', - top: 0 + 'px', - left: 0 + 'px', - width: '1px', - height: '1px', - zIndex: '-999999', - }); - parent.appendChild(testEl); - const testElPosition = testEl.getBoundingClientRect(); - parent.removeChild(testEl); - xformOffsetX = testElPosition.left; - xformOffsetY = testElPosition.top; - // TODO: scale ? - } + // in case ancestor has transform/perspective css properties that change the viewpoint + let xformOffsetX = 0; + let xformOffsetY = 0; + if (parent) { + const testEl = document.createElement('div'); + DDUtils.addElStyles(testEl, { + opacity: '0', + position: 'fixed', + top: 0 + 'px', + left: 0 + 'px', + width: '1px', + height: '1px', + zIndex: '-999999', + }); + parent.appendChild(testEl); + const testElPosition = testEl.getBoundingClientRect(); + parent.removeChild(testEl); + xformOffsetX = testElPosition.left; + xformOffsetY = testElPosition.top; + // TODO: scale ? + } - const targetOffset = el.getBoundingClientRect(); - return { - left: targetOffset.left, - top: targetOffset.top, - offsetLeft: - event.clientX + targetOffset.left - xformOffsetX, - offsetTop: - event.clientY + targetOffset.top - xformOffsetY, - width: targetOffset.width, - height: targetOffset.height - }; - } + const targetOffset = el.getBoundingClientRect(); + return { + left: targetOffset.left, + top: targetOffset.top, + offsetLeft: - event.clientX + targetOffset.left - xformOffsetX, + offsetTop: - event.clientY + targetOffset.top - xformOffsetY, + width: targetOffset.width, + height: targetOffset.height + }; + } - /** @internal TODO: set to public as called by DDDroppable! */ - public ui = (): DDUIData => { - const containmentEl = this.el.parentElement; - const containmentRect = containmentEl.getBoundingClientRect(); - const offset = this.helper.getBoundingClientRect(); - return { - position: { //Current CSS position of the helper as { top, left } object - top: offset.top - containmentRect.top, - left: offset.left - containmentRect.left - } - /* not used by GridStack for now... - helper: [this.helper], //The object arr representing the helper that's being dragged. - offset: { top: offset.top, left: offset.left } // Current offset position of the helper as { top, left } object. - */ - }; - } + /** @internal TODO: set to public as called by DDDroppable! */ + public ui = (): DDUIData => { + const containmentEl = this.el.parentElement; + const containmentRect = containmentEl.getBoundingClientRect(); + const offset = this.helper.getBoundingClientRect(); + return { + position: { //Current CSS position of the helper as { top, left } object + top: offset.top - containmentRect.top, + left: offset.left - containmentRect.left + } + /* not used by GridStack for now... + helper: [this.helper], //The object arr representing the helper that's being dragged. + offset: { top: offset.top, left: offset.left } // Current offset position of the helper as { top, left } object. + */ + }; + } } diff --git a/src/h5/dd-droppable.ts b/src/h5/dd-droppable.ts index 61b78cfee..612b444ea 100644 --- a/src/h5/dd-droppable.ts +++ b/src/h5/dd-droppable.ts @@ -11,189 +11,189 @@ import { GridHTMLElement, GridStack } from '../gridstack'; import { GridItemHTMLElement } from '../types'; export interface DDDroppableOpt { - accept?: string | ((el: HTMLElement) => boolean); - drop?: (event: DragEvent, ui) => void; - over?: (event: DragEvent, ui) => void; - out?: (event: DragEvent, ui) => void; -} + accept?: string | ((el: HTMLElement) => boolean); + drop?: (event: DragEvent, ui) => void; + over?: (event: DragEvent, ui) => void; + out?: (event: DragEvent, ui) => void; + } // TEST let count = 0; export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt { - public accept: (el: HTMLElement) => boolean; - public el: HTMLElement; - public option: DDDroppableOpt; - - /** @internal */ - protected moving: boolean; - protected static lastActive: DDDroppable; - - constructor(el: HTMLElement, opts: DDDroppableOpt = {}) { - super(); - this.el = el; - this.option = opts; - // create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions) - this._dragEnter = this._dragEnter.bind(this); - this._dragOver = this._dragOver.bind(this); - this._dragLeave = this._dragLeave.bind(this); - this._drop = this._drop.bind(this); - - this.el.classList.add('ui-droppable'); - this.el.addEventListener('dragenter', this._dragEnter); - this._setupAccept(); - } - - public on(event: 'drop' | 'dropover' | 'dropout', callback: (event: DragEvent) => void): void { - super.on(event, callback); - } - - public off(event: 'drop' | 'dropover' | 'dropout'): void { - super.off(event); - } - - public enable(): void { - if (!this.disabled) return; - super.enable(); - this.el.classList.remove('ui-droppable-disabled'); - this.el.addEventListener('dragenter', this._dragEnter); - } - - public disable(forDestroy=false): void { - if (this.disabled) return; - super.disable(); - if (!forDestroy) this.el.classList.add('ui-droppable-disabled'); - this.el.removeEventListener('dragenter', this._dragEnter); - } - - public destroy(): void { - this._removeLeaveCallbacks(); - this.disable(true); - this.el.classList.remove('ui-droppable'); - this.el.classList.remove('ui-droppable-disabled'); - super.destroy(); - } - - public updateOption(opts: DDDroppableOpt): DDDroppable { - Object.keys(opts).forEach(key => this.option[key] = opts[key]); - this._setupAccept(); - return this; - } - - /** @internal called when the cursor enters our area - prepare for a possible drop and track leaving */ - protected _dragEnter(event: DragEvent): void { - // TEST console.log(`${count++} Enter ${(this.el as GridHTMLElement).gridstack.opts.id}`); - if (!this._canDrop()) return; - event.preventDefault(); - event.stopPropagation(); - - // ignore multiple 'dragenter' as we go over existing items - if (this.moving) return; - this.moving = true; - - const ev = DDUtils.initEvent(event, { target: this.el, type: 'dropover' }); - if (this.option.over) { - this.option.over(ev, this._ui(DDManager.dragElement)) - } - this.triggerEvent('dropover', ev); - this.el.addEventListener('dragover', this._dragOver); - this.el.addEventListener('drop', this._drop); - this.el.addEventListener('dragleave', this._dragLeave); - // Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome). - // this.el.classList.add('ui-droppable-over'); - - // make sure when we enter this, that the last one gets a leave to correctly cleanup as we don't always do - if (DDDroppable.lastActive && DDDroppable.lastActive !== this) { - DDDroppable.lastActive._dragLeave(event, true); - } - DDDroppable.lastActive = this; - } - - /** @internal called when an moving to drop item is being dragged over - do nothing but eat the event */ - protected _dragOver(event: DragEvent): void { - event.preventDefault(); - event.stopPropagation(); - } - - /** @internal called when the item is leaving our area, stop tracking if we had moving item */ - protected _dragLeave(event: DragEvent, forceLeave?: boolean): void { - // TEST console.log(`${count++} Leave ${(this.el as GridHTMLElement).gridstack.opts.id}`); - event.preventDefault(); - event.stopPropagation(); - - // ignore leave events on our children (we get them when starting to drag our items) - // but exclude nested grids since we would still be leaving ourself, - // but don't handle leave if we're dragging a nested grid around - if (!forceLeave) { - let onChild = DDUtils.inside(event, this.el); - let drag: GridItemHTMLElement = DDManager.dragElement.el; - if (onChild && !drag.gridstackNode?.subGrid) { // dragging a nested grid ? - let nestedEl = (this.el as GridHTMLElement).gridstack.engine.nodes.filter(n => n.subGrid).map(n => (n.subGrid as GridStack).el); - onChild = !nestedEl.some(el => DDUtils.inside(event, el)); - } - if (onChild) return; - } - - if (this.moving) { - const ev = DDUtils.initEvent(event, { target: this.el, type: 'dropout' }); - if (this.option.out) { - this.option.out(ev, this._ui(DDManager.dragElement)) - } - this.triggerEvent('dropout', ev); - } - this._removeLeaveCallbacks(); - - if (DDDroppable.lastActive === this) { - delete DDDroppable.lastActive; - } - } - - /** @internal item is being dropped on us - call the client drop event */ - protected _drop(event: DragEvent): void { - if (!this.moving) return; // should not have received event... - event.preventDefault(); - const ev = DDUtils.initEvent(event, { target: this.el, type: 'drop' }); - if (this.option.drop) { - this.option.drop(ev, this._ui(DDManager.dragElement)) - } - this.triggerEvent('drop', ev); - this._removeLeaveCallbacks(); - } - - /** @internal called to remove callbacks when leaving or dropping */ - protected _removeLeaveCallbacks() { - if (!this.moving) { return; } - delete this.moving; - this.el.removeEventListener('dragover', this._dragOver); - this.el.removeEventListener('drop', this._drop); - this.el.removeEventListener('dragleave', this._dragLeave); - // Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome). - // this.el.classList.remove('ui-droppable-over'); - } - - /** @internal */ - protected _canDrop(): boolean { - return DDManager.dragElement && (!this.accept || this.accept(DDManager.dragElement.el)); - } - - /** @internal */ - protected _setupAccept(): DDDroppable { - if (this.option.accept && typeof this.option.accept === 'string') { - this.accept = (el: HTMLElement) => { - return el.matches(this.option.accept as string) - } - } else { - this.accept = this.option.accept as ((el: HTMLElement) => boolean); - } - return this; - } - - /** @internal */ - protected _ui(drag: DDDraggable) { - return { - draggable: drag.el, - ...drag.ui() - }; - } + public accept: (el: HTMLElement) => boolean; + public el: HTMLElement; + public option: DDDroppableOpt; + + /** @internal */ + protected moving: boolean; + protected static lastActive: DDDroppable; + + constructor(el: HTMLElement, opts: DDDroppableOpt = {}) { + super(); + this.el = el; + this.option = opts; + // create var event binding so we can easily remove and still look like TS methods (unlike anonymous functions) + this._dragEnter = this._dragEnter.bind(this); + this._dragOver = this._dragOver.bind(this); + this._dragLeave = this._dragLeave.bind(this); + this._drop = this._drop.bind(this); + + this.el.classList.add('ui-droppable'); + this.el.addEventListener('dragenter', this._dragEnter); + this._setupAccept(); + } + + public on(event: 'drop' | 'dropover' | 'dropout', callback: (event: DragEvent) => void): void { + super.on(event, callback); + } + + public off(event: 'drop' | 'dropover' | 'dropout'): void { + super.off(event); + } + + public enable(): void { + if (!this.disabled) return; + super.enable(); + this.el.classList.remove('ui-droppable-disabled'); + this.el.addEventListener('dragenter', this._dragEnter); + } + + public disable(forDestroy=false): void { + if (this.disabled) return; + super.disable(); + if (!forDestroy) this.el.classList.add('ui-droppable-disabled'); + this.el.removeEventListener('dragenter', this._dragEnter); + } + + public destroy(): void { + this._removeLeaveCallbacks(); + this.disable(true); + this.el.classList.remove('ui-droppable'); + this.el.classList.remove('ui-droppable-disabled'); + super.destroy(); + } + + public updateOption(opts: DDDroppableOpt): DDDroppable { + Object.keys(opts).forEach(key => this.option[key] = opts[key]); + this._setupAccept(); + return this; + } + + /** @internal called when the cursor enters our area - prepare for a possible drop and track leaving */ + protected _dragEnter(event: DragEvent): void { + // TEST console.log(`${count++} Enter ${(this.el as GridHTMLElement).gridstack.opts.id}`); + if (!this._canDrop()) return; + event.preventDefault(); + event.stopPropagation(); + + // ignore multiple 'dragenter' as we go over existing items + if (this.moving) return; + this.moving = true; + + const ev = DDUtils.initEvent(event, { target: this.el, type: 'dropover' }); + if (this.option.over) { + this.option.over(ev, this._ui(DDManager.dragElement)) + } + this.triggerEvent('dropover', ev); + this.el.addEventListener('dragover', this._dragOver); + this.el.addEventListener('drop', this._drop); + this.el.addEventListener('dragleave', this._dragLeave); + // Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome). + // this.el.classList.add('ui-droppable-over'); + + // make sure when we enter this, that the last one gets a leave to correctly cleanup as we don't always do + if (DDDroppable.lastActive && DDDroppable.lastActive !== this) { + DDDroppable.lastActive._dragLeave(event, true); + } + DDDroppable.lastActive = this; + } + + /** @internal called when an moving to drop item is being dragged over - do nothing but eat the event */ + protected _dragOver(event: DragEvent): void { + event.preventDefault(); + event.stopPropagation(); + } + + /** @internal called when the item is leaving our area, stop tracking if we had moving item */ + protected _dragLeave(event: DragEvent, forceLeave?: boolean): void { + // TEST console.log(`${count++} Leave ${(this.el as GridHTMLElement).gridstack.opts.id}`); + event.preventDefault(); + event.stopPropagation(); + + // ignore leave events on our children (we get them when starting to drag our items) + // but exclude nested grids since we would still be leaving ourself, + // but don't handle leave if we're dragging a nested grid around + if (!forceLeave) { + let onChild = DDUtils.inside(event, this.el); + let drag: GridItemHTMLElement = DDManager.dragElement.el; + if (onChild && !drag.gridstackNode?.subGrid) { // dragging a nested grid ? + let nestedEl = (this.el as GridHTMLElement).gridstack.engine.nodes.filter(n => n.subGrid).map(n => (n.subGrid as GridStack).el); + onChild = !nestedEl.some(el => DDUtils.inside(event, el)); + } + if (onChild) return; + } + + if (this.moving) { + const ev = DDUtils.initEvent(event, { target: this.el, type: 'dropout' }); + if (this.option.out) { + this.option.out(ev, this._ui(DDManager.dragElement)) + } + this.triggerEvent('dropout', ev); + } + this._removeLeaveCallbacks(); + + if (DDDroppable.lastActive === this) { + delete DDDroppable.lastActive; + } + } + + /** @internal item is being dropped on us - call the client drop event */ + protected _drop(event: DragEvent): void { + if (!this.moving) return; // should not have received event... + event.preventDefault(); + const ev = DDUtils.initEvent(event, { target: this.el, type: 'drop' }); + if (this.option.drop) { + this.option.drop(ev, this._ui(DDManager.dragElement)) + } + this.triggerEvent('drop', ev); + this._removeLeaveCallbacks(); + } + + /** @internal called to remove callbacks when leaving or dropping */ + protected _removeLeaveCallbacks() { + if (!this.moving) { return; } + delete this.moving; + this.el.removeEventListener('dragover', this._dragOver); + this.el.removeEventListener('drop', this._drop); + this.el.removeEventListener('dragleave', this._dragLeave); + // Update: removed that as it causes nested grids to no receive dragenter events when parent drags and sets this for #992. not seeing cursor flicker (chrome). + // this.el.classList.remove('ui-droppable-over'); + } + + /** @internal */ + protected _canDrop(): boolean { + return DDManager.dragElement && (!this.accept || this.accept(DDManager.dragElement.el)); + } + + /** @internal */ + protected _setupAccept(): DDDroppable { + if (this.option.accept && typeof this.option.accept === 'string') { + this.accept = (el: HTMLElement) => { + return el.matches(this.option.accept as string) + } + } else { + this.accept = this.option.accept as ((el: HTMLElement) => boolean); + } + return this; + } + + /** @internal */ + protected _ui(drag: DDDraggable) { + return { + draggable: drag.el, + ...drag.ui() + }; + } } diff --git a/src/h5/dd-resizable.ts b/src/h5/dd-resizable.ts index 2befb765c..ff6db532a 100644 --- a/src/h5/dd-resizable.ts +++ b/src/h5/dd-resizable.ts @@ -11,322 +11,326 @@ import { DDUIData, Rect, Size } from '../types'; // TODO: merge with DDDragOpt export interface DDResizableOpt { - autoHide?: boolean; - handles?: string; - maxHeight?: number; - maxWidth?: number; - minHeight?: number; - minWidth?: number; - start?: (event: Event, ui: DDUIData) => void; - stop?: (event: Event) => void; - resize?: (event: Event, ui: DDUIData) => void; -} + autoHide?: boolean; + handles?: string; + maxHeight?: number; + maxWidth?: number; + minHeight?: number; + minWidth?: number; + start?: (event: Event, ui: DDUIData) => void; + stop?: (event: Event) => void; + resize?: (event: Event, ui: DDUIData) => void; + } export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt { - // have to be public else complains for HTMLElementExtendOpt ? - public el: HTMLElement; - public option: DDResizableOpt; + // have to be public else complains for HTMLElementExtendOpt ? + public el: HTMLElement; + public option: DDResizableOpt; + + /** @internal */ + protected handlers: DDResizableHandle[]; + /** @internal */ + protected originalRect: Rect; + /** @internal */ + protected temporalRect: Rect; + /** @internal */ + protected scrollY: number; + /** @internal */ + protected scrolled: number; + /** @internal */ + protected scrollEl: HTMLElement; + /** @internal */ + protected startEvent: MouseEvent; + /** @internal value saved in the same order as _originStyleProp[] */ + protected elOriginStyleVal: string[]; + /** @internal */ + protected parentOriginStylePosition: string; + /** @internal */ + protected static _originStyleProp = ['width', 'height', 'position', 'left', 'top', 'opacity', 'zIndex']; - /** @internal */ - protected handlers: DDResizableHandle[]; - /** @internal */ - protected originalRect: Rect; - /** @internal */ - protected temporalRect: Rect; - /** @internal */ - protected scrollY: number; - /** @internal */ - protected scrolled: number; - /** @internal */ - protected scrollEl: HTMLElement; - /** @internal */ - protected startEvent: MouseEvent; - /** @internal value saved in the same order as _originStyleProp[] */ - protected elOriginStyleVal: string[]; - /** @internal */ - protected parentOriginStylePosition: string; - /** @internal */ - protected static _originStyleProp = ['width', 'height', 'position', 'left', 'top', 'opacity', 'zIndex']; + constructor(el: HTMLElement, opts: DDResizableOpt = {}) { + super(); + this.el = el; + this.option = opts; + this.enable(); + this._setupAutoHide(); + this._setupHandlers(); + } - constructor(el: HTMLElement, opts: DDResizableOpt = {}) { - super(); - this.el = el; - this.option = opts; - this.enable(); - this._setupAutoHide(); - this._setupHandlers(); - } + public on(event: 'resizestart' | 'resize' | 'resizestop', callback: (event: DragEvent) => void): void { + super.on(event, callback); + } - public on(event: 'resizestart' | 'resize' | 'resizestop', callback: (event: DragEvent) => void): void { - super.on(event, callback); - } + public off(event: 'resizestart' | 'resize' | 'resizestop'): void { + super.off(event); + } - public off(event: 'resizestart' | 'resize' | 'resizestop'): void { - super.off(event); - } + public enable(): void { + super.enable(); + this.el.classList.add('ui-resizable'); + this.el.classList.remove('ui-resizable-disabled'); + } - public enable(): void { - super.enable(); - this.el.classList.add('ui-resizable'); - this.el.classList.remove('ui-resizable-disabled'); - } + public disable(): void { + super.disable(); + this.el.classList.add('ui-resizable-disabled'); + this.el.classList.remove('ui-resizable'); + } - public disable(): void { - super.disable(); - this.el.classList.add('ui-resizable-disabled'); - this.el.classList.remove('ui-resizable'); - } + public destroy(): void { + this._removeHandlers(); + if (this.option.autoHide) { + this.el.removeEventListener('mouseover', this._showHandlers); + this.el.removeEventListener('mouseout', this._hideHandlers); + } + this.el.classList.remove('ui-resizable'); + delete this.el; + super.destroy(); + } - public destroy(): void { - this._removeHandlers(); - if (this.option.autoHide) { - this.el.removeEventListener('mouseover', this._showHandlers); - this.el.removeEventListener('mouseout', this._hideHandlers); - } - this.el.classList.remove('ui-resizable'); - delete this.el; - super.destroy(); - } + public updateOption(opts: DDResizableOpt): DDResizable { + let updateHandles = (opts.handles && opts.handles !== this.option.handles); + let updateAutoHide = (opts.autoHide && opts.autoHide !== this.option.autoHide); + Object.keys(opts).forEach(key => this.option[key] = opts[key]); + if (updateHandles) { + this._removeHandlers(); + this._setupHandlers(); + } + if (updateAutoHide) { + this._setupAutoHide(); + } + return this; + } - public updateOption(opts: DDResizableOpt): DDResizable { - let updateHandles = (opts.handles && opts.handles !== this.option.handles); - let updateAutoHide = (opts.autoHide && opts.autoHide !== this.option.autoHide); - Object.keys(opts).forEach(key => this.option[key] = opts[key]); - if (updateHandles) { - this._removeHandlers(); - this._setupHandlers(); - } - if (updateAutoHide) { - this._setupAutoHide(); - } - return this; - } + /** @internal */ + protected _setupAutoHide(): DDResizable { + if (this.option.autoHide) { + this.el.classList.add('ui-resizable-autohide'); + // use mouseover/mouseout instead of mouseenter/mouseleave to get better performance; + this.el.addEventListener('mouseover', this._showHandlers); + this.el.addEventListener('mouseout', this._hideHandlers); + } else { + this.el.classList.remove('ui-resizable-autohide'); + this.el.removeEventListener('mouseover', this._showHandlers); + this.el.removeEventListener('mouseout', this._hideHandlers); + } + return this; + } - /** @internal */ - protected _setupAutoHide(): DDResizable { - if (this.option.autoHide) { - this.el.classList.add('ui-resizable-autohide'); - // use mouseover/mouseout instead of mouseenter/mouseleave to get better performance; - this.el.addEventListener('mouseover', this._showHandlers); - this.el.addEventListener('mouseout', this._hideHandlers); - } else { - this.el.classList.remove('ui-resizable-autohide'); - this.el.removeEventListener('mouseover', this._showHandlers); - this.el.removeEventListener('mouseout', this._hideHandlers); - } - return this; - } + /** @internal */ + protected _showHandlers = () => { + this.el.classList.remove('ui-resizable-autohide'); + } - /** @internal */ - protected _showHandlers = () => { - this.el.classList.remove('ui-resizable-autohide'); - } + /** @internal */ + protected _hideHandlers = () => { + this.el.classList.add('ui-resizable-autohide'); + } - /** @internal */ - protected _hideHandlers = () => { - this.el.classList.add('ui-resizable-autohide'); - } + /** @internal */ + protected _setupHandlers(): DDResizable { + let handlerDirection = this.option.handles || 'e,s,se'; + if (handlerDirection === 'all') { + handlerDirection = 'n,e,s,w,se,sw,ne,nw'; + } + this.handlers = handlerDirection.split(',') + .map(dir => dir.trim()) + .map(dir => new DDResizableHandle(this.el, dir, { + start: (event: MouseEvent) => { + this._resizeStart(event); + }, + stop: (event: MouseEvent) => { + this._resizeStop(event); + }, + move: (event: MouseEvent) => { + this._resizing(event, dir); + } + })); + return this; + } - /** @internal */ - protected _setupHandlers(): DDResizable { - let handlerDirection = this.option.handles || 'e,s,se'; - if (handlerDirection === 'all') { - handlerDirection = 'n,e,s,w,se,sw,ne,nw'; - } - this.handlers = handlerDirection.split(',') - .map(dir => dir.trim()) - .map(dir => new DDResizableHandle(this.el, dir, { - start: (event: MouseEvent) => { - this._resizeStart(event); - }, - stop: (event: MouseEvent) => { - this._resizeStop(event); - }, - move: (event: MouseEvent) => { - this._resizing(event, dir); - } - })); - return this; - } + /** @internal */ + protected _resizeStart(event: MouseEvent): DDResizable { + this.originalRect = this.el.getBoundingClientRect(); + this.scrollEl = Utils.getScrollElement(this.el); + this.scrollY = this.scrollEl.scrollTop; + this.scrolled = 0; + this.startEvent = event; + this._setupHelper(); + this._applyChange(); + const ev = DDUtils.initEvent(event, { type: 'resizestart', target: this.el }); + if (this.option.start) { + this.option.start(ev, this._ui()); + } + this.el.classList.add('ui-resizable-resizing'); + this.triggerEvent('resizestart', ev); + return this; + } - /** @internal */ - protected _resizeStart(event: MouseEvent): DDResizable { - this.originalRect = this.el.getBoundingClientRect(); - this.scrollEl = Utils.getScrollElement(this.el); - this.scrollY = this.scrollEl.scrollTop; - this.scrolled = 0; - this.startEvent = event; - this._setupHelper(); - this._applyChange(); - const ev = DDUtils.initEvent(event, { type: 'resizestart', target: this.el }); - if (this.option.start) { - this.option.start(ev, this._ui()); - } - this.el.classList.add('ui-resizable-resizing'); - this.triggerEvent('resizestart', ev); - return this; - } + /** @internal */ + protected _resizing(event: MouseEvent, dir: string): DDResizable { + this.scrolled = this.scrollEl.scrollTop - this.scrollY; + this.temporalRect = this._getChange(event, dir); + this._applyChange(); + const ev = DDUtils.initEvent(event, { type: 'resize', target: this.el }); + if (this.option.resize) { + this.option.resize(ev, this._ui()); + } + this.triggerEvent('resize', ev); + return this; + } - /** @internal */ - protected _resizing(event: MouseEvent, dir: string): DDResizable { - this.scrolled = this.scrollEl.scrollTop - this.scrollY; - this.temporalRect = this._getChange(event, dir); - this._applyChange(); - const ev = DDUtils.initEvent(event, { type: 'resize', target: this.el }); - if (this.option.resize) { - this.option.resize(ev, this._ui()); - } - this.triggerEvent('resize', ev); - return this; - } + /** @internal */ + protected _resizeStop(event: MouseEvent): DDResizable { + const ev = DDUtils.initEvent(event, { type: 'resizestop', target: this.el }); + if (this.option.stop) { + this.option.stop(ev); // Note: ui() not used by gridstack so don't pass + } + this.el.classList.remove('ui-resizable-resizing'); + this.triggerEvent('resizestop', ev); + this._cleanHelper(); + delete this.startEvent; + delete this.originalRect; + delete this.temporalRect; + delete this.scrollY; + delete this.scrolled; + return this; + } - /** @internal */ - protected _resizeStop(event: MouseEvent): DDResizable { - const ev = DDUtils.initEvent(event, { type: 'resizestop', target: this.el }); - if (this.option.stop) { - this.option.stop(ev); // Note: ui() not used by gridstack so don't pass - } - this.el.classList.remove('ui-resizable-resizing'); - this.triggerEvent('resizestop', ev); - this._cleanHelper(); - delete this.startEvent; - delete this.originalRect; - delete this.temporalRect; - delete this.scrollY; - delete this.scrolled; - return this; - } + /** @internal */ + protected _setupHelper(): DDResizable { + this.elOriginStyleVal = DDResizable._originStyleProp.map(prop => this.el.style[prop]); + this.parentOriginStylePosition = this.el.parentElement.style.position; + if (window.getComputedStyle(this.el.parentElement).position.match(/static/)) { + this.el.parentElement.style.position = 'relative'; + } + this.el.style.position = 'absolute'; + this.el.style.opacity = '0.8'; + return this; + } - /** @internal */ - protected _setupHelper(): DDResizable { - this.elOriginStyleVal = DDResizable._originStyleProp.map(prop => this.el.style[prop]); - this.parentOriginStylePosition = this.el.parentElement.style.position; - if (window.getComputedStyle(this.el.parentElement).position.match(/static/)) { - this.el.parentElement.style.position = 'relative'; - } - this.el.style.position = 'absolute'; - this.el.style.opacity = '0.8'; - return this; - } + /** @internal */ + protected _cleanHelper(): DDResizable { + // don't restore position, width and, height styles when constructable stylesheet is not supported we are updating these directly on elements + if (!Utils.isConstructableStyleSheetSupported()) { + DDResizable._originStyleProp = DDResizable._originStyleProp.filter(x => !["left", "right", "top", "bottom", "position", "height", "width"].includes(x)); + } + DDResizable._originStyleProp.forEach((prop, i) => { + this.el.style[prop] = this.elOriginStyleVal[i] || null; + }); + this.el.parentElement.style.position = this.parentOriginStylePosition || null; + return this; + } - /** @internal */ - protected _cleanHelper(): DDResizable { - DDResizable._originStyleProp.forEach((prop, i) => { - this.el.style[prop] = this.elOriginStyleVal[i] || null; - }); - this.el.parentElement.style.position = this.parentOriginStylePosition || null; - return this; - } + /** @internal */ + protected _getChange(event: MouseEvent, dir: string): Rect { + const oEvent = this.startEvent; + const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out. + width: this.originalRect.width, + height: this.originalRect.height + this.scrolled, + left: this.originalRect.left, + top: this.originalRect.top - this.scrolled + }; - /** @internal */ - protected _getChange(event: MouseEvent, dir: string): Rect { - const oEvent = this.startEvent; - const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out. - width: this.originalRect.width, - height: this.originalRect.height + this.scrolled, - left: this.originalRect.left, - top: this.originalRect.top - this.scrolled - }; - - const offsetX = event.clientX - oEvent.clientX; - const offsetY = event.clientY - oEvent.clientY; + const offsetX = event.clientX - oEvent.clientX; + const offsetY = event.clientY - oEvent.clientY; - if (dir.indexOf('e') > -1) { - newRect.width += offsetX; - } else if (dir.indexOf('w') > -1) { - newRect.width -= offsetX; - newRect.left += offsetX; - } - if (dir.indexOf('s') > -1) { - newRect.height += offsetY; - } else if (dir.indexOf('n') > -1) { - newRect.height -= offsetY; - newRect.top += offsetY - } - const constrain = this._constrainSize(newRect.width, newRect.height); - if (Math.round(newRect.width) !== Math.round(constrain.width)) { // round to ignore slight round-off errors - if (dir.indexOf('w') > -1) { - newRect.left += newRect.width - constrain.width; - } - newRect.width = constrain.width; - } - if (Math.round(newRect.height) !== Math.round(constrain.height)) { - if (dir.indexOf('n') > -1) { - newRect.top += newRect.height - constrain.height; - } - newRect.height = constrain.height; - } - return newRect; - } + if (dir.indexOf('e') > -1) { + newRect.width += offsetX; + } else if (dir.indexOf('w') > -1) { + newRect.width -= offsetX; + newRect.left += offsetX; + } + if (dir.indexOf('s') > -1) { + newRect.height += offsetY; + } else if (dir.indexOf('n') > -1) { + newRect.height -= offsetY; + newRect.top += offsetY + } + const constrain = this._constrainSize(newRect.width, newRect.height); + if (Math.round(newRect.width) !== Math.round(constrain.width)) { // round to ignore slight round-off errors + if (dir.indexOf('w') > -1) { + newRect.left += newRect.width - constrain.width; + } + newRect.width = constrain.width; + } + if (Math.round(newRect.height) !== Math.round(constrain.height)) { + if (dir.indexOf('n') > -1) { + newRect.top += newRect.height - constrain.height; + } + newRect.height = constrain.height; + } + return newRect; + } - /** @internal constrain the size to the set min/max values */ - protected _constrainSize(oWidth: number, oHeight: number): Size { - const maxWidth = this.option.maxWidth || Number.MAX_SAFE_INTEGER; - const minWidth = this.option.minWidth || oWidth; - const maxHeight = this.option.maxHeight || Number.MAX_SAFE_INTEGER; - const minHeight = this.option.minHeight || oHeight; - const width = Math.min(maxWidth, Math.max(minWidth, oWidth)); - const height = Math.min(maxHeight, Math.max(minHeight, oHeight)); - return { width, height }; - } + /** @internal constrain the size to the set min/max values */ + protected _constrainSize(oWidth: number, oHeight: number): Size { + const maxWidth = this.option.maxWidth || Number.MAX_SAFE_INTEGER; + const minWidth = this.option.minWidth || oWidth; + const maxHeight = this.option.maxHeight || Number.MAX_SAFE_INTEGER; + const minHeight = this.option.minHeight || oHeight; + const width = Math.min(maxWidth, Math.max(minWidth, oWidth)); + const height = Math.min(maxHeight, Math.max(minHeight, oHeight)); + return { width, height }; + } - /** @internal */ - protected _applyChange(): DDResizable { - let containmentRect = { left: 0, top: 0, width: 0, height: 0 }; - if (this.el.style.position === 'absolute') { - const containmentEl = this.el.parentElement; - const { left, top } = containmentEl.getBoundingClientRect(); - containmentRect = { left, top, width: 0, height: 0 }; - } - if (!this.temporalRect) return this; - Object.keys(this.temporalRect).forEach(key => { - const value = this.temporalRect[key]; - this.el.style[key] = value - containmentRect[key] + 'px'; - }); - return this; - } + /** @internal */ + protected _applyChange(): DDResizable { + let containmentRect = { left: 0, top: 0, width: 0, height: 0 }; + if (this.el.style.position === 'absolute') { + const containmentEl = this.el.parentElement; + const { left, top } = containmentEl.getBoundingClientRect(); + containmentRect = { left, top, width: 0, height: 0 }; + } + if (!this.temporalRect) return this; + Object.keys(this.temporalRect).forEach(key => { + const value = this.temporalRect[key]; + this.el.style[key] = value - containmentRect[key] + 'px'; + }); + return this; + } - /** @internal */ - protected _removeHandlers(): DDResizable { - this.handlers.forEach(handle => handle.destroy()); - delete this.handlers; - return this; - } + /** @internal */ + protected _removeHandlers(): DDResizable { + this.handlers.forEach(handle => handle.destroy()); + delete this.handlers; + return this; + } - /** @internal */ - protected _ui = (): DDUIData => { - const containmentEl = this.el.parentElement; - const containmentRect = containmentEl.getBoundingClientRect(); - const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out. - width: this.originalRect.width, - height: this.originalRect.height + this.scrolled, - left: this.originalRect.left, - top: this.originalRect.top - this.scrolled - }; - const rect = this.temporalRect || newRect; - return { - position: { - left: rect.left - containmentRect.left, - top: rect.top - containmentRect.top - }, - size: { - width: rect.width, - height: rect.height - } - /* Gridstack ONLY needs position set above... keep around in case. - element: [this.el], // The object representing the element to be resized - helper: [], // TODO: not support yet - The object representing the helper that's being resized - originalElement: [this.el],// we don't wrap here, so simplify as this.el //The object representing the original element before it is wrapped - originalPosition: { // The position represented as { left, top } before the resizable is resized - left: this.originalRect.left - containmentRect.left, - top: this.originalRect.top - containmentRect.top - }, - originalSize: { // The size represented as { width, height } before the resizable is resized - width: this.originalRect.width, - height: this.originalRect.height - } - */ - }; - } + /** @internal */ + protected _ui = (): DDUIData => { + const containmentEl = this.el.parentElement; + const containmentRect = containmentEl.getBoundingClientRect(); + const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out. + width: this.originalRect.width, + height: this.originalRect.height + this.scrolled, + left: this.originalRect.left, + top: this.originalRect.top - this.scrolled + }; + const rect = this.temporalRect || newRect; + return { + position: { + left: rect.left - containmentRect.left, + top: rect.top - containmentRect.top + }, + size: { + width: rect.width, + height: rect.height + } + /* Gridstack ONLY needs position set above... keep around in case. + element: [this.el], // The object representing the element to be resized + helper: [], // TODO: not support yet - The object representing the helper that's being resized + originalElement: [this.el],// we don't wrap here, so simplify as this.el //The object representing the original element before it is wrapped + originalPosition: { // The position represented as { left, top } before the resizable is resized + left: this.originalRect.left - containmentRect.left, + top: this.originalRect.top - containmentRect.top + }, + originalSize: { // The size represented as { width, height } before the resizable is resized + width: this.originalRect.width, + height: this.originalRect.height + } + */ + }; + } } diff --git a/src/types.ts b/src/types.ts index 5f0d0190b..27d3a1177 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,28 +8,29 @@ import { GridStackEngine } from './gridstack-engine'; /** different layout options when changing # of columns, - * including a custom function that takes new/old column count, and array of new/old positions - * Note: new list may be partially already filled if we have a cache of the layout at that size and new items were added later. - */ + * including a custom function that takes new/old column count, and array of new/old positions + * Note: new list may be partially already filled if we have a cache of the layout at that size and new items were added later. + */ export type ColumnOptions = 'moveScale' | 'move' | 'scale' | 'none' | - ((column: number, oldColumn: number, nodes: GridStackNode[], oldNodes: GridStackNode[]) => void); + ((column: number, oldColumn: number, nodes: GridStackNode[], oldNodes: GridStackNode[]) => void); export type numberOrString = number | string; export interface GridItemHTMLElement extends HTMLElement { - /** pointer to grid node instance */ - gridstackNode?: GridStackNode; - /** @internal */ - _gridstackNodeOrig?: GridStackNode; -} + /** pointer to grid node instance */ + gridstackNode?: GridStackNode; + /** @internal */ + _gridstackNodeOrig?: GridStackNode; + } export type GridStackElement = string | HTMLElement | GridItemHTMLElement; export type GridStackEventHandlerCallback = (event: Event, arg2?: GridItemHTMLElement | GridStackNode | GridStackNode[], newNode?: GridStackNode) => void; /** - * Defines the options for a Grid - */ + * Defines the options for a Grid + */ export interface GridStackOptions { + /** * accept widgets dragged from other grids or from outside (default: `false`). Can be: * `true` (uses `'.grid-stack-item'` class filter) or `false`, @@ -115,253 +116,223 @@ export interface GridStackOptions { /** 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; + /** minimum rows amount. Default is `0`. You can also do this with `min-height` CSS attribute + * on the grid div in pixels, which will round to the closest row. + */ + minRow?: number; - /** id used to debug grid instance, not currently stored in DOM attributes */ - id?: numberOrString; + /** minimal width before grid will be shown in one column mode (default?: 768) */ + oneColumnSize?: number; - /** additional widget class (default?: 'grid-stack-item') */ - itemClass?: string; - - /** - * 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. Default is `0`. You can also do this with `min-height` CSS attribute - * on the grid div in pixels, which will round to the closest row. - */ - minRow?: number; - - /** minimal width before grid will be shown in one column mode (default?: 768) */ - oneColumnSize?: number; - - /** - * set to true if you want oneColumnMode to use the DOM order and ignore x,y from normal multi column - * layouts during sorting. This enables you to have custom 1 column layout that differ from the rest. (default?: false) - */ - oneColumnModeDomSort?: boolean; + /** + * set to true if you want oneColumnMode to use the DOM order and ignore x,y from normal multi column + * layouts during sorting. This enables you to have custom 1 column layout that differ from the rest. (default?: false) + */ + oneColumnModeDomSort?: boolean; - /** class for placeholder (default?: 'grid-stack-placeholder') */ - placeholderClass?: string; + /** class for placeholder (default?: 'grid-stack-placeholder') */ + placeholderClass?: string; - /** placeholder default content (default?: '') */ - placeholderText?: string; + /** placeholder default content (default?: '') */ + placeholderText?: string; - /** allows to override UI resizable options. (default?: { autoHide: true, handles: 'se' }) */ - resizable?: DDResizeOpt; + /** allows to override UI resizable options. (default?: { autoHide: true, handles: 'se' }) */ + resizable?: DDResizeOpt; - /** - * if true widgets could be removed by dragging outside of the grid. It could also be a selector string (ex: ".trash"), - * in this case widgets will be removed by dropping them there (default?: false) - * See example (http://gridstack.github.io/gridstack.js/demo/two.html) - */ - removable?: boolean | string; + /** + * if true widgets could be removed by dragging outside of the grid. It could also be a selector string (ex: ".trash"), + * in this case widgets will be removed by dropping them there (default?: false) + * See example (http://gridstack.github.io/gridstack.js/demo/two.html) + */ + removable?: boolean | string; - /** allows to override UI removable options. (default?: { accept: '.grid-stack-item' }) */ - removableOptions?: DDRemoveOpt; + /** allows to override UI removable options. (default?: { accept: '.grid-stack-item' }) */ + removableOptions?: DDRemoveOpt; - /** fix grid number of rows. This is a shortcut of writing `minRow:N, maxRow:N`. (default `0` no constrain) */ - row?: number; + /** fix grid number of rows. This is a shortcut of writing `minRow:N, maxRow:N`. (default `0` no constrain) */ + row?: number; - /** - * if true turns grid to RTL. Possible values are true, false, 'auto' (default?: 'auto') - * See [example](http://gridstack.github.io/gridstack.js/demo/rtl.html) - */ - rtl?: boolean | 'auto'; + /** + * if true turns grid to RTL. Possible values are true, false, 'auto' (default?: 'auto') + * See [example](http://gridstack.github.io/gridstack.js/demo/rtl.html) + */ + rtl?: boolean | 'auto'; - /** - * makes grid static (default?: false). If `true` widgets are not movable/resizable. - * You don't even need draggable/resizable. A CSS class - * 'grid-stack-static' is also added to the element. - */ - staticGrid?: boolean; + /** + * makes grid static (default?: false). If `true` widgets are not movable/resizable. + * You don't even need draggable/resizable. A CSS class + * 'grid-stack-static' is also added to the element. + */ + staticGrid?: boolean; - /** if `true` will add style element to `` otherwise will add it to element's parent node (default `false`). */ - styleInHead?: boolean; + /** if `true` will add style element to `` otherwise will add it to element's parent node (default `false`). */ + styleInHead?: boolean; - /** @internal point to a parent grid item if we're nested */ - _isNested?: GridStackNode; - /** @internal unique class name for our generated CSS style sheet */ - _styleSheetClass?: string; -} + /** @internal point to a parent grid item if we're nested */ + _isNested?: GridStackNode; + /** @internal unique class name for our generated CSS style sheet */ + _styleSheetClass?: string; + } /** options used during GridStackEngine.moveNode() */ export interface GridStackMoveOpts extends GridStackPosition { - /** node to skip collision */ - skip?: GridStackNode; - /** do we pack (default true) */ - pack?: boolean; - /** true if we are calling this recursively to prevent simple swap or coverage collision - default false*/ - nested?: boolean; - /** vars to calculate other cells coordinates */ - cellWidth?: number; - cellHeight?: number; - marginTop?: number; - marginBottom?: number; - marginLeft?: number; - marginRight?: number; - /** position in pixels of the currently dragged items (for overlap check) */ - rect?: GridStackPosition; - /** true if we're live resizing */ - resizing?: boolean; -} + /** node to skip collision */ + skip?: GridStackNode; + /** do we pack (default true) */ + pack?: boolean; + /** true if we are calling this recursively to prevent simple swap or coverage collision - default false*/ + nested?: boolean; + /** vars to calculate other cells coordinates */ + cellWidth?: number; + cellHeight?: number; + marginTop?: number; + marginBottom?: number; + marginLeft?: number; + marginRight?: number; + /** position in pixels of the currently dragged items (for overlap check) */ + rect?: GridStackPosition; + /** true if we're live resizing */ + resizing?: boolean; + } export interface GridStackPosition { - /** widget position x (default?: 0) */ - x?: number; - /** widget position y (default?: 0) */ - y?: number; - /** widget dimension width (default?: 1) */ - w?: number; - /** widget dimension height (default?: 1) */ - h?: number; -} + /** widget position x (default?: 0) */ + x?: number; + /** widget position y (default?: 0) */ + y?: number; + /** widget dimension width (default?: 1) */ + w?: number; + /** widget dimension height (default?: 1) */ + h?: number; + } /** - * GridStack Widget creation options - */ + * GridStack Widget creation options + */ export interface GridStackWidget extends GridStackPosition { - /** if true then x, y parameters will be ignored and widget will be places on the first available position (default?: false) */ - autoPosition?: boolean; - /** minimum width allowed during resize/creation (default?: undefined = un-constrained) */ - minW?: number; - /** maximum width allowed during resize/creation (default?: undefined = un-constrained) */ - maxW?: number; - /** minimum height allowed during resize/creation (default?: undefined = un-constrained) */ - minH?: number; - /** maximum height allowed during resize/creation (default?: undefined = un-constrained) */ - maxH?: number; - /** prevent resizing (default?: undefined = un-constrained) */ - noResize?: boolean; - /** prevents moving (default?: undefined = un-constrained) */ - noMove?: boolean; - /** prevents being moved by others during their (default?: undefined = un-constrained) */ - locked?: boolean; - /** widgets can have their own custom resize handles. For example 'e,w' will make that particular widget only resize east and west. See `resizable: {handles: string}` option */ - resizeHandles?: string; - /** value for `gs-id` stored on the widget (default?: undefined) */ - id?: numberOrString; - /** html to append inside as content */ - content?: string; - /** optional nested grid options and list of children, which then turns into actual instance at runtime */ - subGrid?: GridStackOptions | GridStack; -} + /** if true then x, y parameters will be ignored and widget will be places on the first available position (default?: false) */ + autoPosition?: boolean; + /** minimum width allowed during resize/creation (default?: undefined = un-constrained) */ + minW?: number; + /** maximum width allowed during resize/creation (default?: undefined = un-constrained) */ + maxW?: number; + /** minimum height allowed during resize/creation (default?: undefined = un-constrained) */ + minH?: number; + /** maximum height allowed during resize/creation (default?: undefined = un-constrained) */ + maxH?: number; + /** prevent resizing (default?: undefined = un-constrained) */ + noResize?: boolean; + /** prevents moving (default?: undefined = un-constrained) */ + noMove?: boolean; + /** prevents being moved by others during their (default?: undefined = un-constrained) */ + locked?: boolean; + /** widgets can have their own custom resize handles. For example 'e,w' will make that particular widget only resize east and west. See `resizable: {handles: string}` option */ + resizeHandles?: string; + /** value for `gs-id` stored on the widget (default?: undefined) */ + id?: numberOrString; + /** html to append inside as content */ + content?: string; + /** optional nested grid options and list of children, which then turns into actual instance at runtime */ + subGrid?: GridStackOptions | GridStack; + } /** Drag&Drop resize options */ export interface DDResizeOpt { - /** do resize handle hide by default until mouse over ? - default: true */ - autoHide?: boolean; - /** - * sides where you can resize from (ex: 'e, se, s, sw, w') - default 'se' (south-east) - * Note: it is not recommended to resize from the top sides as weird side effect may occur. - */ - handles?: string; -} + /** do resize handle hide by default until mouse over ? - default: true */ + autoHide?: boolean; + /** + * sides where you can resize from (ex: 'e, se, s, sw, w') - default 'se' (south-east) + * Note: it is not recommended to resize from the top sides as weird side effect may occur. + */ + handles?: string; + } /** Drag&Drop remove options */ export interface DDRemoveOpt { - /** class that can be removed (default?: '.' + opts.itemClass) */ - accept?: string; -} + /** class that can be removed (default?: '.' + opts.itemClass) */ + accept?: string; + } /** Drag&Drop dragging options */ export interface DDDragOpt { - /** class selector of items that can be dragged. default to '.grid-stack-item-content' */ - handle?: string; - /** default to `true` */ - scroll?: boolean; - /** default to 'body' */ - appendTo?: string; - /** parent constraining where item can be dragged out from (default: null = no constrain) */ - containment?: string; -} + /** class selector of items that can be dragged. default to '.grid-stack-item-content' */ + handle?: string; + /** default to `true` */ + scroll?: boolean; + /** default to 'body' */ + appendTo?: string; + /** parent constraining where item can be dragged out from (default: null = no constrain) */ + containment?: string; + } export interface DDDragInOpt extends DDDragOpt { - /** used when dragging item from the outside, and canceling (ex: 'invalid' or your own method)*/ - revert?: string | ((event: Event) => HTMLElement); - /** helper function when dropping (ex: 'clone' or your own method) */ - helper?: string | ((event: Event) => HTMLElement); -} + /** used when dragging item from the outside, and canceling (ex: 'invalid' or your own method)*/ + revert?: string | ((event: Event) => HTMLElement); + /** helper function when dropping (ex: 'clone' or your own method) */ + helper?: string | ((event: Event) => HTMLElement); + } export interface Size { - width: number; - height: number; -} + width: number; + height: number; + } export interface Position { - top: number; - left: number; -} + top: number; + left: number; + } export interface Rect extends Size, Position {} /** data that is passed during drag and resizing callbacks */ export interface DDUIData { - position?: Position; - size?: Size; - /* fields not used by GridStack but sent by jq ? leave in case we go back to them... - originalPosition? : Position; - offset?: Position; - originalSize?: Size; - element?: HTMLElement[]; - helper?: HTMLElement[]; - originalElement?: HTMLElement[]; - */ -} + position?: Position; + size?: Size; + /* fields not used by GridStack but sent by jq ? leave in case we go back to them... + originalPosition? : Position; + offset?: Position; + originalSize?: Size; + element?: HTMLElement[]; + helper?: HTMLElement[]; + originalElement?: HTMLElement[]; + */ + } /** - * internal descriptions describing the items in the grid - */ + * internal descriptions describing the items in the grid + */ export interface GridStackNode extends GridStackWidget { - /** pointer back to HTML element */ - el?: GridItemHTMLElement; - /** pointer back to Grid instance */ - grid?: GridStack; - /** @internal internal id used to match when cloning engines or saving column layouts */ - _id?: number; - /** @internal */ - _dirty?: boolean; - /** @internal */ - _updating?: boolean; - /** @internal true when over trash/another grid so we don't bother removing drag CSS style that would animate back to old position */ - _isAboutToRemove?: boolean; - /** @internal true if item came from outside of the grid -> actual item need to be moved over */ - _isExternal?: boolean; - /** @internal moving vs resizing */ - _moving?: boolean; - /** @internal true if we jumped down past item below (one time jump so we don't have to totally pass it) */ - _skipDown?: boolean; - /** @internal original values before a drag/size */ - _orig?: GridStackPosition; - /** @internal position in pixels used during collision check */ - _rect?: GridStackPosition; - /** @internal top/left pixel location before a drag so we can detect direction of move from last position*/ - _lastUiPosition?: Position; - /** @internal set on the item being dragged/resized remember the last positions we've tried (but failed) so we don't try again during drag/resize */ - _lastTried?: GridStackPosition; - /** @internal position willItFit() will use to position the item */ - _willFitPos?: GridStackPosition; - /** @internal last drag Y pixel position used to incrementally update V scroll bar */ - _prevYPix?: number; - /** @internal true if we've remove the item from ourself (dragging out) but might revert it back (release on nothing -> goes back) */ - _temporaryRemoved?: boolean; - /** @internal true if we should remove DOM element on _notify() rather than clearing _id (old way) */ - _removeDOM?: boolean; - /** @internal */ - _initDD?: boolean; -} + /** pointer back to HTML element */ + el?: GridItemHTMLElement; + /** pointer back to Grid instance */ + grid?: GridStack; + /** @internal internal id used to match when cloning engines or saving column layouts */ + _id?: number; + /** @internal */ + _dirty?: boolean; + /** @internal */ + _updating?: boolean; + /** @internal true when over trash/another grid so we don't bother removing drag CSS style that would animate back to old position */ + _isAboutToRemove?: boolean; + /** @internal true if item came from outside of the grid -> actual item need to be moved over */ + _isExternal?: boolean; + /** @internal moving vs resizing */ + _moving?: boolean; + /** @internal true if we jumped down past item below (one time jump so we don't have to totally pass it) */ + _skipDown?: boolean; + /** @internal original values before a drag/size */ + _orig?: GridStackPosition; + /** @internal position in pixels used during collision check */ + _rect?: GridStackPosition; + /** @internal top/left pixel location before a drag so we can detect direction of move from last position*/ + _lastUiPosition?: Position; + /** @internal set on the item being dragged/resized remember the last positions we've tried (but failed) so we don't try again during drag/resize */ + _lastTried?: GridStackPosition; + /** @internal position willItFit() will use to position the item */ + _willFitPos?: GridStackPosition; + /** @internal last drag Y pixel position used to incrementally update V scroll bar */ + _prevYPix?: number; + /** @internal true if we've remove the item from ourself (dragging out) but might revert it back (release on nothing -> goes back) */ + _temporaryRemoved?: boolean; + /** @internal true if we should remove DOM element on _notify() rather than clearing _id (old way) */ + _removeDOM?: boolean; + /** @internal */ + _initDD?: boolean; + } diff --git a/src/utils.ts b/src/utils.ts index 3e152b8e7..4567e7a4d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,16 +6,17 @@ import { GridStackElement, GridStackNode, GridStackOptions, numberOrString, GridStackPosition, GridStackWidget } from './types'; export interface HeightData { - h: number; - unit: string; -} + h: number; + unit: string; + } /** checks for obsolete method names */ // eslint-disable-next-line -export function obsolete(self, f, oldName: string, newName: string, rev: string): (...args: any[]) => any { + export function obsolete(self, f, oldName: string, newName: string, rev: string): (...args: any[]) => any { let wrapper = (...args) => { console.warn('gridstack.js: Function `' + oldName + '` is deprecated in ' + rev + ' and has been replaced ' + 'with `' + newName + '`. It will be **removed** in a future release'); + return f.apply(self, args); } wrapper.prototype = f.prototype; @@ -49,8 +50,8 @@ export function obsoleteAttr(el: HTMLElement, oldName: string, newName: string, } /** - * Utility methods - */ + * Utility methods + */ export class Utils { /** convert a potential selector into actual list of html elements */ @@ -101,11 +102,11 @@ export class Utils { return Utils.isIntercepted(a, {x: b.x-0.5, y: b.y-0.5, w: b.w+1, h: b.h+1}) } /** - * Sorts array of nodes - * @param nodes array to sort - * @param dir 1 for asc, -1 for desc (optional) - * @param width width of the grid. If undefined the width will be calculated automatically (optional). - **/ + * Sorts array of nodes + * @param nodes array to sort + * @param dir 1 for asc, -1 for desc (optional) + * @param width width of the grid. If undefined the width will be calculated automatically (optional). + **/ static sort(nodes: GridStackNode[], dir?: -1 | 1, column?: number): GridStackNode[] { column = column || nodes.reduce((col, n) => Math.max(n.x + n.w, col), 0) || 12; if (dir === -1) @@ -115,44 +116,96 @@ export class Utils { } /** - * creates a style sheet with style id under given parent - * @param id will set the 'gs-style-id' attribute to that id - * @param parent to insert the stylesheet as first child, - * if none supplied it will be appended to the document head instead. - */ - static createStylesheet(id: string, parent?: HTMLElement): CSSStyleSheet { - let style: HTMLStyleElement = document.createElement('style'); - style.setAttribute('type', 'text/css'); - style.setAttribute('gs-style-id', id); + * creates a style sheet with style id under given parent + * @param id will set the 'gs-style-id' attribute to that id + * @param parent to insert the stylesheet as first child, + * if none supplied it will be appended to the document head instead. + */ + static createStylesheet(parent?: HTMLElement): CSSStyleSheet { + let stylesheet: CSSStyleSheet = new CSSStyleSheet(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((style as any).styleSheet) { // TODO: only CSSImportRule have that and different beast ?? - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (style as any).styleSheet.cssText = ''; + const location: any = parent? parent.getRootNode() : document; + if(!location.adoptedStyleSheets || location.adoptedStyleSheets.length === 0) { + location.adoptedStyleSheets = [stylesheet]; } else { - style.appendChild(document.createTextNode('')); // WebKit hack + location.adoptedStyleSheets = [...location.adoptedStyleSheets, stylesheet]; } - if (!parent) { - // default to head - parent = document.getElementsByTagName('head')[0]; - parent.appendChild(style); - } else { - parent.insertBefore(style, parent.firstChild); - } - return style.sheet as CSSStyleSheet; + return stylesheet; } /** removed the given stylesheet id */ - static removeStylesheet(id: string): void { - let el = document.querySelector('STYLE[gs-style-id=' + id + ']'); - if (el && el.parentNode) el.remove(); + static removeStylesheet(stylesheet: CSSStyleSheet): void { + const styles = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const docObj: any = document; + docObj.adoptedStyleSheets.forEach((style: CSSStyleSheet) => { + styles.push(style); + }); + styles.splice(styles.indexOf(stylesheet),1); + docObj.adoptedStyleSheets = styles; } /** inserts a CSS rule */ - static addCSSRule(sheet: CSSStyleSheet, selector: string, rules: string): void { - if (typeof sheet.addRule === 'function') { - sheet.addRule(selector, rules); - } else if (typeof sheet.insertRule === 'function') { - sheet.insertRule(`${selector}{${rules}}`); + static addCSSRule(sheet: any, selector: string, rules: string): void { + if (typeof sheet.replaceSync === 'function') { + sheet.replaceSync(Utils.concatCSSRules(sheet, selector, rules)); + } + } + + /** concat css rules in existing stylesheet */ + static concatCSSRules(sheet: CSSStyleSheet, selector: string, rules: string): string { + return Object.values(sheet.cssRules).reduce((acc: string, curr: CSSRule) => { + return acc + curr.cssText; + }, '') + ' ' + selector + '{' + rules + '}'; + } + /** update CSS style on elements with given selector */ + static updateStyleOnElements(selector: string | HTMLElement[], styles: {[props: string]: string | string[] }): void { + const elements = typeof selector === 'string' ? Utils.getElements(selector) : selector; + for (const el of elements) { + Utils.addElementStyle(el, styles); + } + } + + /** update CSS style on element */ + static addElementStyle(el: HTMLElement, styles: {[props: string]: string | string[] }): void { + if(!el) { + return; + } + if (styles) { + for (const s in styles) { + if (styles.hasOwnProperty(s)) { + el.style[s] = styles[s]; + } + } + } + } + + /** update position style on element */ + static updatePositionStyleOnWidget(el: HTMLElement, cellHeight: number, cellHeightUnit: string): void { + const h = Utils.toNumber(el.getAttribute('gs-h')); + const minH= Utils.toNumber(el.getAttribute('gs-min-h')); + const maxH = Utils.toNumber(el.getAttribute('gs-max-h')); + const y = Utils.toNumber(el.getAttribute('gs-y')); + el.style.height = Utils.getCSSHeight(h, cellHeight, cellHeightUnit); + el.style.minHeight = Utils.getCSSHeight(minH, cellHeight, cellHeightUnit); + el.style.maxHeight = Utils.getCSSHeight(maxH, cellHeight, cellHeightUnit); + el.style.top = Utils.getCSSHeight(y, cellHeight, cellHeightUnit); + } + + /** returns CSS height of a given row */ + static getCSSHeight(rows: number, cellHeight: number, cellHeightUnit: string): string { + return (rows * cellHeight as number) + cellHeightUnit; + } + + static isConstructableStyleSheetSupported(): boolean { + try { + let stylesheet = new CSSStyleSheet(); + if ('replaceSync' in stylesheet) { + return true; + } + return false; + } catch (error) { + return false; } } @@ -190,7 +243,7 @@ export class Utils { /** copies unset fields in target to use the given default sources values */ // eslint-disable-next-line - static defaults(target, ...sources): {} { + static defaults(target, ...sources): {} { sources.forEach(source => { for (const key in source) { @@ -313,7 +366,7 @@ export class Utils { let rect = el.getBoundingClientRect(); let innerHeightOrClientHeight = (window.innerHeight || document.documentElement.clientHeight); if (rect.top < 0 || - rect.bottom > innerHeightOrClientHeight + rect.bottom > innerHeightOrClientHeight ) { // set scrollTop of first parent that scrolls // if parent is larger than el, set as low as possible @@ -345,12 +398,12 @@ export class Utils { } /** - * @internal Function used to scroll the page. - * - * @param event `MouseEvent` that triggers the resize - * @param el `HTMLElement` that's being resized - * @param distance Distance from the V edges to start scrolling - */ + * @internal Function used to scroll the page. + * + * @param event `MouseEvent` that triggers the resize + * @param el `HTMLElement` that's being resized + * @param distance Distance from the V edges to start scrolling + */ static updateScrollResize(event: MouseEvent, el: HTMLElement, distance: number): void { const scrollEl = this.getScrollElement(el); const height = scrollEl.clientHeight; @@ -386,9 +439,9 @@ export class Utils { } /** - * Recursive clone version that returns a full copy, checking for nested objects and arrays ONLY. - * Note: this will use as-is any key starting with double __ (and not copy inside) some lib have circular dependencies. - */ + * Recursive clone version that returns a full copy, checking for nested objects and arrays ONLY. + * Note: this will use as-is any key starting with double __ (and not copy inside) some lib have circular dependencies. + */ static cloneDeep(obj: T): T { // return JSON.parse(JSON.stringify(obj)); // doesn't work with date format ? const ret = Utils.clone(obj);