diff --git a/src/dragdrop/dd-base-impl.ts b/src/dragdrop/dd-base-impl.ts index 34b271559..a8fc18065 100644 --- a/src/dragdrop/dd-base-impl.ts +++ b/src/dragdrop/dd-base-impl.ts @@ -7,26 +7,32 @@ */ export type EventCallback = (event: Event) => boolean|void; export abstract class DDBaseImplement { - disabled = false; + protected disabled = false; private eventRegister: { [eventName: string]: EventCallback; } = {}; - on(event: string, callback: EventCallback): void { + + public on(event: string, callback: EventCallback): void { this.eventRegister[event] = callback; } - off(event: string) { + + public off(event: string): void { delete this.eventRegister[event]; } - enable(): void { + + public enable(): void { this.disabled = false; } - disable(): void { + + public disable(): void { this.disabled = true; } - destroy() { - this.eventRegister = undefined; + + public destroy(): void { + delete this.eventRegister; } - triggerEvent(eventName: string, event: Event): boolean|void { + + public triggerEvent(eventName: string, event: Event): boolean|void { if (this.disabled) { return; } if (!this.eventRegister) {return; } // used when destroy before triggerEvent fire if (this.eventRegister[eventName]) { @@ -38,5 +44,5 @@ export abstract class DDBaseImplement { export interface HTMLElementExtendOpt { el: HTMLElement; option: T; - updateOption(T): void; + updateOption(T): DDBaseImplement; } diff --git a/src/dragdrop/dd-draggable.ts b/src/dragdrop/dd-draggable.ts index 5805ee2d1..89c00849d 100644 --- a/src/dragdrop/dd-draggable.ts +++ b/src/dragdrop/dd-draggable.ts @@ -8,6 +8,7 @@ import { DDManager } from './dd-manager'; import { DDUtils } from './dd-utils'; import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl'; +import { DDUIData } from '../types'; export interface DDDraggableOpt { appendTo?: string | HTMLElement; @@ -15,82 +16,101 @@ export interface DDDraggableOpt { 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 | ((event: Event) => HTMLElement); + helper?: string | HTMLElement | ((event: Event) => HTMLElement); basePosition?: 'fixed' | 'absolute'; - start?: (event?, ui?) => void; - stop?: (event?, ui?) => void; - drag?: (event?, ui?) => void; -}; + start?: (event: Event, ui: DDUIData) => void; + stop?: (event: Event) => void; + drag?: (event: Event, ui: DDUIData) => void; +} + +export interface DragOffset { + left: number; + top: number; + width: number; + height: number; + offsetLeft: number; + offsetTop: number; +} + export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt { - static basePosition: 'fixed' | 'absolute' = 'absolute'; - static dragEventListenerOption = DDUtils.isEventSupportPassiveOption ? { capture: true, passive: true } : true; - static originStyleProp = ['transition', 'pointerEvents', 'position', + public el: HTMLElement; + public helper: HTMLElement; + public option: DDDraggableOpt; + public dragOffset: DragOffset; + public dragElementOriginStyle: Array; + public dragFollowTimer: number; + public mouseDownElement: HTMLElement; + public dragging = false; + public paintTimer: number; + public parentOriginStylePosition: string; + public helperContainment: HTMLElement; + + private static basePosition: 'fixed' | 'absolute' = 'absolute'; + private static dragEventListenerOption = DDUtils.isEventSupportPassiveOption ? { capture: true, passive: true } : true; + private static originStyleProp = ['transition', 'pointerEvents', 'position', 'left', 'top', 'opacity', 'zIndex', 'width', 'height', 'willChange']; - el: HTMLElement; - helper: HTMLElement; - option: DDDraggableOpt; - dragOffset: { - left: number; - top: number; - width: number; - height: number; - offsetLeft: number; - offsetTop: number; - }; - dragElementOriginStyle: Array; - dragFollowTimer: number; - mouseDownElement: HTMLElement; - dragging = false; - paintTimer: number; - parentOriginStylePosition: string; - helperContainment: HTMLElement; - constructor(el: HTMLElement, option: DDDraggableOpt) { + constructor(el: HTMLElement, option: DDDraggableOpt = {}) { super(); this.el = el; - this.option = option || {}; + this.option = option; this.init(); } - on(event: 'drag' | 'dragstart' | 'dragstop', callback: (event: DragEvent) => void): void { + public on(event: 'drag' | 'dragstart' | 'dragstop', callback: (event: DragEvent) => void): void { super.on(event, callback); } - off(event: 'drag' | 'dragstart' | 'dragstop') { + public off(event: 'drag' | 'dragstart' | 'dragstop'): void { super.off(event); } - enable() { + public enable(): void { super.enable(); this.el.draggable = true; this.el.classList.remove('ui-draggable-disabled'); } - disable() { + public disable(): void { super.disable(); this.el.draggable = false; this.el.classList.add('ui-draggable-disabled'); } - updateOption(opts) { - Object.keys(opts).forEach(key => { - const value = opts[key]; - this.option[key] = value; - }); + 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.el.draggable = false; + this.el.classList.remove('ui-draggable'); + this.el.removeEventListener('dragstart', this.dragStart); + 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; } - protected init() { + protected init(): DDDraggable { this.el.draggable = true; this.el.classList.add('ui-draggable'); this.el.addEventListener('mousedown', this.mouseDown); this.el.addEventListener('dragstart', this.dragStart); + return this; } - protected mouseDown = (event: MouseEvent) => { + private mouseDown = (event: MouseEvent): void => { this.mouseDownElement = event.target as HTMLElement; } - protected dragStart = (event: DragEvent) => { + private dragStart = (event: DragEvent): void => { if (this.option.handle && !( this.mouseDownElement && this.mouseDownElement.matches( @@ -106,17 +126,17 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt 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.setupDragFollowNodeNNotifyStart(ev); + this.setupDragFollowNodeNotifyStart(ev); } else { this.dragFollowTimer = window.setTimeout(() => { - this.dragFollowTimer = undefined; - this.setupDragFollowNodeNNotifyStart(ev); + delete this.dragFollowTimer; + this.setupDragFollowNodeNotifyStart(ev); }, 0); } this.cancelDragGhost(event); } - protected setupDragFollowNodeNNotifyStart(ev) { + protected setupDragFollowNodeNotifyStart(ev: Event): DDDraggable { this.setupHelperStyle(); document.addEventListener('dragover', this.drag, DDDraggable.dragEventListenerOption); this.el.addEventListener('dragend', this.dragEnd); @@ -126,9 +146,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt this.dragging = true; this.helper.classList.add('ui-draggable-dragging'); this.triggerEvent('dragstart', ev); + return this; } - protected drag = (event: DragEvent) => { + private drag = (event: DragEvent): void => { this.dragFollow(event); const ev = DDUtils.initEvent(event, { target: this.el, type: 'drag' }); if (this.option.drag) { @@ -137,10 +158,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt this.triggerEvent('drag', ev); } - protected dragEnd = (event: DragEvent) => { + private dragEnd = (event: DragEvent): void => { if (this.dragFollowTimer) { clearTimeout(this.dragFollowTimer); - this.dragFollowTimer = undefined; + delete this.dragFollowTimer; return; } else { if (this.paintTimer) { @@ -159,15 +180,15 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt } const ev = DDUtils.initEvent(event, { target: this.el, type: 'dragstop' }); if (this.option.stop) { - this.option.stop(ev, this.ui()); + this.option.stop(ev); // Note: ui() not used by gridstack so don't pass } this.triggerEvent('dragstop', ev); - DDManager.dragElement = undefined; - this.helper = undefined; - this.mouseDownElement = undefined; + delete DDManager.dragElement; + delete this.helper; + delete this.mouseDownElement; } - private createHelper(event: DragEvent) { + private createHelper(event: DragEvent): HTMLElement { const helperIsFunction = (typeof this.option.helper) === 'function'; const helper = (helperIsFunction ? (this.option.helper as ((event: Event) => HTMLElement)).apply(this.el, [event]) @@ -184,7 +205,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt return helper; } - private setupHelperStyle() { + private setupHelperStyle(): DDDraggable { this.helper.style.pointerEvents = 'none'; this.helper.style.width = this.dragOffset.width + 'px'; this.helper.style.height = this.dragOffset.height + 'px'; @@ -197,21 +218,23 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt this.helper.style.transition = null; // recover animation } }, 0); + return this; } - private removeHelperStyle() { + private removeHelperStyle(): DDDraggable { DDDraggable.originStyleProp.forEach(prop => { this.helper.style[prop] = this.dragElementOriginStyle[prop] || null; }); - this.dragElementOriginStyle = undefined; + delete this.dragElementOriginStyle; + return this; } - private dragFollow = (event: DragEvent) => { + private dragFollow = (event: DragEvent): void => { if (this.paintTimer) { cancelAnimationFrame(this.paintTimer); } this.paintTimer = requestAnimationFrame(() => { - this.paintTimer = undefined; + delete this.paintTimer; const offset = this.dragOffset; let containmentRect = { left: 0, top: 0 }; if (this.helper.style.position === 'absolute') { @@ -223,7 +246,7 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt }); } - private setupHelperContainmentStyle() { + private setupHelperContainmentStyle(): DDDraggable { this.helperContainment = this.helper.parentElement; if (this.option.basePosition !== 'fixed') { this.parentOriginStylePosition = this.helperContainment.style.position; @@ -231,9 +254,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt this.helperContainment.style.position = 'relative'; } } + return this; } - private cancelDragGhost(e: DragEvent) { + private cancelDragGhost(e: DragEvent): DDDraggable { if (e.dataTransfer != null) { e.dataTransfer.setData('text', ''); } @@ -250,12 +274,15 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt return; } e.stopPropagation(); + return this; } - private getDragOffset(event: DragEvent, el: HTMLElement, attachedParent: HTMLElement) { + private getDragOffset(event: DragEvent, el: HTMLElement, parent: HTMLElement): DragOffset { + // in case ancestor has transform/perspective css properties that change the viewpoint - const getViewPointFromParent = (parent) => { - if (!parent) { return null; } + let xformOffsetX = 0; + let xformOffsetY = 0; + if (parent) { const testEl = document.createElement('div'); DDUtils.addElStyles(testEl, { opacity: '0', @@ -269,53 +296,36 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt parent.appendChild(testEl); const testElPosition = testEl.getBoundingClientRect(); parent.removeChild(testEl); - return { - offsetX: testElPosition.left, - offsetY: testElPosition.top - }; + xformOffsetX = testElPosition.left; + xformOffsetY = testElPosition.top; + // TODO: scale ? } + const targetOffset = el.getBoundingClientRect(); - const mousePositionXY = { - x: event.clientX, - y: event.clientY - }; - const transformOffset = getViewPointFromParent(attachedParent); return { left: targetOffset.left, top: targetOffset.top, - offsetLeft: - mousePositionXY.x + targetOffset.left - transformOffset.offsetX, - offsetTop: - mousePositionXY.y + targetOffset.top - transformOffset.offsetY, + offsetLeft: - event.clientX + targetOffset.left - xformOffsetX, + offsetTop: - event.clientY + targetOffset.top - xformOffsetY, width: targetOffset.width, height: targetOffset.height }; } - destroy() { - 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.el.draggable = false; - this.el.classList.remove('ui-draggable'); - this.el.removeEventListener('dragstart', this.dragStart); - this.el = undefined; - this.helper = undefined; - this.option = undefined; - super.destroy(); - } - ui = () => { + /** public -> called by DDDroppable as well */ + public ui = (): DDUIData => { const containmentEl = this.el.parentElement; const containmentRect = containmentEl.getBoundingClientRect(); const offset = this.helper.getBoundingClientRect(); return { - helper: [this.helper], //The object arr representing the helper that's being dragged. - position: { + position: { //Current CSS position of the helper as { top, left } object top: offset.top - containmentRect.top, left: offset.left - containmentRect.left - }, //Current CSS position of the helper as { top, left } object + } + /* 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/dragdrop/dd-droppable.ts b/src/dragdrop/dd-droppable.ts index 45670ba1d..6bc2e4bf5 100644 --- a/src/dragdrop/dd-droppable.ts +++ b/src/dragdrop/dd-droppable.ts @@ -15,54 +15,72 @@ export interface DDDroppableOpt { drop?: (event: DragEvent, ui) => void; over?: (event: DragEvent, ui) => void; out?: (event: DragEvent, ui) => void; -}; +} + export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt { - accept: (el: HTMLElement) => boolean; - el: HTMLElement; - option: DDDroppableOpt; + + public accept: (el: HTMLElement) => boolean; + public el: HTMLElement; + public option: DDDroppableOpt; private acceptable: boolean = null; private style; - constructor(el: HTMLElement, opts: DDDroppableOpt) { + + constructor(el: HTMLElement, opts: DDDroppableOpt = {}) { super(); this.el = el; - this.option = opts || {}; + this.option = opts; this.init(); } - on(event: 'drop' | 'dropover' | 'dropout', callback: (event: DragEvent) => void): void { + + public on(event: 'drop' | 'dropover' | 'dropout', callback: (event: DragEvent) => void): void { super.on(event, callback); } - off(event: 'drop' | 'dropover' | 'dropout') { + + public off(event: 'drop' | 'dropover' | 'dropout'): void { super.off(event); } - enable() { + + public enable(): void { if (!this.disabled) { return; } super.enable(); this.el.classList.remove('ui-droppable-disabled'); this.el.addEventListener('dragenter', this.dragEnter); } - disable() { + + public disable(): void { if (this.disabled) { return; } super.disable(); this.el.classList.add('ui-droppable-disabled'); this.el.removeEventListener('dragenter', this.dragEnter); } - updateOption(opts) { - Object.keys(opts).forEach(key => { - const value = opts[key]; - this.option[key] = value; - }); + + public destroy(): void { + this.el.classList.remove('ui-droppable'); + if (this.disabled) { + this.el.classList.remove('ui-droppable-disabled'); + this.el.removeEventListener('dragenter', this.dragEnter); + this.el.removeEventListener('dragover', this.dragOver); + this.el.removeEventListener('drop', this.drop); + this.el.removeEventListener('dragleave', this.dragLeave); + } + super.destroy(); + } + + public updateOption(opts: DDDroppableOpt): DDDroppable { + Object.keys(opts).forEach(key => this.option[key] = opts[key]); this.setupAccept(); + return this; } - protected init() { + protected init(): DDDroppable { this.el.classList.add('ui-droppable'); this.el.addEventListener('dragenter', this.dragEnter); - this.setupAccept(); this.createStyleSheet(); + return this; } - protected dragEnter = (event: DragEvent) => { + protected dragEnter = (event: DragEvent): void => { this.el.removeEventListener('dragenter', this.dragEnter); this.acceptable = this.canDrop(); if (this.acceptable) { @@ -77,14 +95,15 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt } this.el.classList.add('ui-droppable-over'); this.el.addEventListener('dragleave', this.dragLeave); - } - protected dragOver = (event: DragEvent) => { + + private dragOver = (event: DragEvent): void => { event.preventDefault(); event.stopPropagation(); } - protected dragLeave = (event: DragEvent) => { - if (this.el.contains(event.relatedTarget as HTMLElement)) { return; }; + + private dragLeave = (event: DragEvent): void => { + if (this.el.contains(event.relatedTarget as HTMLElement)) { return; } this.el.removeEventListener('dragleave', this.dragLeave); this.el.classList.remove('ui-droppable-over'); if (this.acceptable) { @@ -100,7 +119,7 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt this.el.addEventListener('dragenter', this.dragEnter); } - protected drop = (event: DragEvent) => { + private drop = (event: DragEvent): void => { if (this.acceptable) { event.preventDefault(); const ev = DDUtils.initEvent(event, { target: this.el, type: 'drop' }); @@ -117,10 +136,12 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt }); } } - private canDrop() { + + private canDrop(): boolean { return DDManager.dragElement && (!this.accept || this.accept(DDManager.dragElement.el)); } - private setupAccept() { + + private setupAccept(): DDDroppable { if (this.option.accept && typeof this.option.accept === 'string') { this.accept = (el: HTMLElement) => { return el.matches(this.option.accept as string) @@ -128,34 +149,27 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt } else { this.accept = this.option.accept as ((el: HTMLElement) => boolean); } + return this; } - private createStyleSheet() { + // TODO: share this with other instances and when do remove ??? + private createStyleSheet(): DDDroppable { const content = `.ui-droppable.ui-droppable-over > *:not(.ui-droppable) {pointer-events: none;}`; this.style = document.createElement('style'); this.style.innerText = content; this.el.appendChild(this.style); + return this; } + + // TODO: not call private removeStyleSheet() { this.el.removeChild(this.style); } - destroy() { - this.el.classList.remove('ui-droppable'); - if (this.disabled) { - this.el.classList.remove('ui-droppable-disabled'); - this.el.removeEventListener('dragenter', this.dragEnter); - this.el.removeEventListener('dragover', this.dragOver); - this.el.removeEventListener('drop', this.drop); - this.el.removeEventListener('dragleave', this.dragLeave); - } - super.destroy(); - } - - ui(DDDraggable: DDDraggable) { + private ui(drag: DDDraggable) { return { - draggable: DDDraggable.el, - ...DDDraggable.ui() + draggable: drag.el, + ...drag.ui() }; } } diff --git a/src/dragdrop/dd-element.ts b/src/dragdrop/dd-element.ts index 36eb41f85..13e23a65f 100644 --- a/src/dragdrop/dd-element.ts +++ b/src/dragdrop/dd-element.ts @@ -9,85 +9,97 @@ import { DDResizable, DDResizableOpt } from './dd-resizable'; import { GridItemHTMLElement } from './../types'; import { DDDraggable, DDDraggableOpt } from './dd-draggable'; import { DDDroppable, DDDroppableOpt } from './dd-droppable'; + export interface DDElementHost extends GridItemHTMLElement { ddElement?: DDElement; } + export class DDElement { - static init(el) { - el.ddElement = new DDElement(el); + + static init(el: DDElementHost): DDElement { + if (!el.ddElement) { el.ddElement = new DDElement(el); } return el.ddElement; } - el: DDElementHost; - ddDraggable?: DDDraggable; - ddDroppable?: DDDroppable; - ddResizable?: DDResizable; + + public el: DDElementHost; + public ddDraggable?: DDDraggable; + public ddDroppable?: DDDroppable; + public ddResizable?: DDResizable; + constructor(el: DDElementHost) { this.el = el; } - on(eventName: string, callback: (event: MouseEvent) => void) { + + public on(eventName: string, callback: (event: MouseEvent) => void): DDElement { if (this.ddDraggable && ['drag', 'dragstart', 'dragstop'].indexOf(eventName) > -1) { this.ddDraggable.on(eventName as 'drag' | 'dragstart' | 'dragstop', callback); - return; - } - if (this.ddDroppable && ['drop', 'dropover', 'dropout'].indexOf(eventName) > -1) { + } else if (this.ddDroppable && ['drop', 'dropover', 'dropout'].indexOf(eventName) > -1) { this.ddDroppable.on(eventName as 'drop' | 'dropover' | 'dropout', callback); - return; - } - if (this.ddResizable && ['resizestart', 'resize', 'resizestop'].indexOf(eventName) > -1) { + } else if (this.ddResizable && ['resizestart', 'resize', 'resizestop'].indexOf(eventName) > -1) { this.ddResizable.on(eventName as 'resizestart' | 'resize' | 'resizestop', callback); - return; } - return; + return this; } - off(eventName: string) { + + public off(eventName: string): DDElement { if (this.ddDraggable && ['drag', 'dragstart', 'dragstop'].indexOf(eventName) > -1) { this.ddDraggable.off(eventName as 'drag' | 'dragstart' | 'dragstop'); - return; - } - if (this.ddDroppable && ['drop', 'dropover', 'dropout'].indexOf(eventName) > -1) { + } else if (this.ddDroppable && ['drop', 'dropover', 'dropout'].indexOf(eventName) > -1) { this.ddDroppable.off(eventName as 'drop' | 'dropover' | 'dropout'); - return; - } - if (this.ddResizable && ['resizestart', 'resize', 'resizestop'].indexOf(eventName) > -1) { + } else if (this.ddResizable && ['resizestart', 'resize', 'resizestop'].indexOf(eventName) > -1) { this.ddResizable.off(eventName as 'resizestart' | 'resize' | 'resizestop'); - return; } - return; + return this; } - setupDraggable(opts: DDDraggableOpt) { + + public setupDraggable(opts: DDDraggableOpt): DDElement { if (!this.ddDraggable) { this.ddDraggable = new DDDraggable(this.el, opts); } else { this.ddDraggable.updateOption(opts); } + return this; + } + + public cleanDraggable(): DDElement { + if (this.ddDraggable) { + this.ddDraggable.destroy(); + delete this.ddDraggable; + } + return this; } - setupResizable(opts: DDResizableOpt) { + + public setupResizable(opts: DDResizableOpt): DDElement { if (!this.ddResizable) { this.ddResizable = new DDResizable(this.el, opts); } else { this.ddResizable.updateOption(opts); } + return this; } - cleanDraggable() { - if (!this.ddDraggable) { return; } - this.ddDraggable.destroy(); - this.ddDraggable = undefined; + + public cleanResizable(): DDElement { + if (this.ddResizable) { + this.ddResizable.destroy(); + delete this.ddResizable; + } + return this; } - setupDroppable(opts: DDDroppableOpt) { + + public setupDroppable(opts: DDDroppableOpt): DDElement { if (!this.ddDroppable) { this.ddDroppable = new DDDroppable(this.el, opts); } else { this.ddDroppable.updateOption(opts); } + return this; } - cleanDroppable() { - if (!this.ddDroppable) { return; } - this.ddDroppable.destroy(); - this.ddDroppable = undefined; - } - cleanResizable() { - if (!this.cleanResizable) { return; } - this.ddResizable.destroy(); - this.ddResizable = undefined; + + public cleanDroppable(): DDElement { + if (this.ddDroppable) { + this.ddDroppable.destroy(); + delete this.ddDroppable; + } + return this; } } diff --git a/src/dragdrop/dd-manager.ts b/src/dragdrop/dd-manager.ts index 702bd93a8..efc77a357 100644 --- a/src/dragdrop/dd-manager.ts +++ b/src/dragdrop/dd-manager.ts @@ -6,6 +6,7 @@ * gridstack.js may be freely distributed under the MIT license. */ import { DDDraggable } from './dd-draggable'; + export class DDManager { static dragElement: DDDraggable; } diff --git a/src/dragdrop/dd-resizable-handle.ts b/src/dragdrop/dd-resizable-handle.ts index 60fa65b6a..acc3562cc 100644 --- a/src/dragdrop/dd-resizable-handle.ts +++ b/src/dragdrop/dd-resizable-handle.ts @@ -10,15 +10,17 @@ export interface DDResizableHandleOpt { move?: (event) => void; stop?: (event) => void; } + export class DDResizableHandle { - static prefix = 'ui-resizable-'; - el: HTMLElement; - host: HTMLElement; - option: DDResizableHandleOpt; - dir: string; + public el: HTMLElement; + public host: HTMLElement; + public option: DDResizableHandleOpt; + public dir: string; private mouseMoving = false; private started = false; private mouseDownEvent: MouseEvent; + private static prefix = 'ui-resizable-'; + constructor(host: HTMLElement, direction: string, option: DDResizableHandleOpt) { this.host = host; this.dir = direction; @@ -26,7 +28,7 @@ export class DDResizableHandle { this.init(); } - init() { + public init(): DDResizableHandle { const el = document.createElement('div'); el.classList.add('ui-resizable-handle'); el.classList.add(`${DDResizableHandle.prefix}${this.dir}`); @@ -35,9 +37,10 @@ export class DDResizableHandle { this.el = el; this.host.appendChild(this.el); this.el.addEventListener('mousedown', this.mouseDown); + return this; } - protected mouseDown = (event: MouseEvent) => { + private mouseDown = (event: MouseEvent): void => { this.mouseDownEvent = event; setTimeout(() => { document.addEventListener('mousemove', this.mouseMove, true); @@ -46,13 +49,13 @@ export class DDResizableHandle { if (!this.mouseMoving) { document.removeEventListener('mousemove', this.mouseMove, true); document.removeEventListener('mouseup', this.mouseUp); - this.mouseDownEvent = undefined; + delete this.mouseDownEvent; } }, 300); }, 100); } - protected mouseMove = (event: MouseEvent) => { + private mouseMove = (event: MouseEvent): void => { if (!this.started && !this.mouseMoving) { if (this.hasMoved(event, this.mouseDownEvent)) { this.mouseMoving = true; @@ -65,7 +68,7 @@ export class DDResizableHandle { } } - protected mouseUp = (event: MouseEvent) => { + private mouseUp = (event: MouseEvent): void => { if (this.mouseMoving) { this.triggerEvent('stop', event); } @@ -73,10 +76,10 @@ export class DDResizableHandle { document.removeEventListener('mouseup', this.mouseUp); this.mouseMoving = false; this.started = false; - this.mouseDownEvent = undefined; + delete this.mouseDownEvent; } - private hasMoved(event: MouseEvent, oEvent: MouseEvent) { + private hasMoved(event: MouseEvent, oEvent: MouseEvent): boolean { const { clientX, clientY } = event; const { clientX: oClientX, clientY: oClientY } = oEvent; return ( @@ -85,22 +88,15 @@ export class DDResizableHandle { ); } - show() { - this.el.style.display = 'block'; - } - - hide() { - this.el.style.display = 'none'; - } - - destroy() { + public destroy(): DDResizableHandle { this.host.removeChild(this.el); + return this; } - triggerEvent(name: string, event: MouseEvent) { + private triggerEvent(name: string, event: MouseEvent): DDResizableHandle { if (this.option[name]) { this.option[name](event); } + return this; } - } diff --git a/src/dragdrop/dd-resizable.ts b/src/dragdrop/dd-resizable.ts index d13a619e3..0c9c3faac 100644 --- a/src/dragdrop/dd-resizable.ts +++ b/src/dragdrop/dd-resizable.ts @@ -8,6 +8,8 @@ import { DDResizableHandle } from './dd-resizable-handle'; import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl'; import { DDUtils } from './dd-utils'; +import { DDUIData, Rect, Size } from '../types'; + export interface DDResizableOpt { autoHide?: boolean; handles?: string; @@ -16,56 +18,69 @@ export interface DDResizableOpt { minHeight?: number; minWidth?: number; basePosition?: 'fixed' | 'absolute'; - start?: (event: MouseEvent, ui) => void; - stop?: (event: MouseEvent, ui) => void; - resize?: (event: MouseEvent, ui) => void; + start?: (event: Event, ui: DDUIData) => void; + stop?: (event: Event) => void; + resize?: (event: Event, ui: DDUIData) => void; } + export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt { - static originStyleProp = ['width', 'height', 'position', 'left', 'top', 'opacity', 'zIndex']; - el: HTMLElement; - option: DDResizableOpt; - handlers: DDResizableHandle[]; - helper: HTMLElement; - originalRect; - temporalRect; + + public el: HTMLElement; + public option: DDResizableOpt; + public handlers: DDResizableHandle[]; + public helper: HTMLElement; + public originalRect: Rect; + public temporalRect: Rect; + private startEvent: MouseEvent; private elOriginStyle; private parentOriginStylePosition; - constructor(el: HTMLElement, opts: DDResizableOpt) { + private static originStyleProp = ['width', 'height', 'position', 'left', 'top', 'opacity', 'zIndex']; + + constructor(el: HTMLElement, opts: DDResizableOpt = {}) { super(); this.el = el; - this.option = opts || {}; + this.option = opts; this.init(); } - on(event: 'resizestart' | 'resize' | 'resizestop', callback: (event: DragEvent) => void): void { + + public on(event: 'resizestart' | 'resize' | 'resizestop', callback: (event: DragEvent) => void): void { super.on(event, callback); } - off(event: 'resizestart' | 'resize' | 'resizestop') { + + public off(event: 'resizestart' | 'resize' | 'resizestop'): void { super.off(event); } - enable() { - if (!this.disabled) { return; } - super.enable(); - this.el.classList.remove('ui-resizable-disabled'); - } - disable() { - if (this.disabled) { return; } - super.disable(); - this.el.classList.add('ui-resizable-disabled'); + + public enable(): void { + if (this.disabled) { + super.enable(); + this.el.classList.remove('ui-resizable-disabled'); + } } - updateOption(opts: DDResizableOpt) { - let updateHandles = false; - let updateAutoHide = false; - if (opts.handles !== this.option.handles) { - updateHandles = true; + + public disable(): void { + if (!this.disabled) { + super.disable(); + this.el.classList.add('ui-resizable-disabled'); } - if (opts.autoHide !== this.option.autoHide) { - updateAutoHide = true; + } + + public destroy(): void { + this.removeHandlers(); + if (this.option.autoHide) { + this.el.removeEventListener('mouseover', this.showHandlers); + this.el.removeEventListener('mouseout', this.hideHandlers); } - Object.keys(opts).forEach(key => { - const value = opts[key]; - this.option[key] = value; - }); + 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(); @@ -73,15 +88,17 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt if (updateAutoHide) { this.setupAutoHide(); } + return this; } - protected init() { + protected init(): DDResizable { this.el.classList.add('ui-resizable'); this.setupAutoHide(); this.setupHandlers(); + return this; } - protected setupAutoHide() { + 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; @@ -92,17 +109,18 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt this.el.removeEventListener('mouseover', this.showHandlers); this.el.removeEventListener('mouseout', this.hideHandlers); } + return this; } - protected showHandlers = () => { + private showHandlers = () => { this.el.classList.remove('ui-resizable-autohide'); } - protected hideHandlers = () => { + private hideHandlers = () => { this.el.classList.add('ui-resizable-autohide'); } - protected setupHandlers() { + protected setupHandlers(): DDResizable { let handlerDirection = this.option.handles || 'e,s,se'; if (handlerDirection === 'all') { handlerDirection = 'n,e,s,w,se,sw,ne,nw'; @@ -120,9 +138,10 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt this.resizing(event, dir); } })); + return this; } - protected resizeStart(event: MouseEvent) { + protected resizeStart(event: MouseEvent): DDResizable { this.originalRect = this.el.getBoundingClientRect(); this.startEvent = event; this.setupHelper(); @@ -133,9 +152,10 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt } this.el.classList.add('ui-resizable-resizing'); this.triggerEvent('resizestart', ev); + return this; } - protected resizing(event: MouseEvent, dir: string) { + protected resizing(event: MouseEvent, dir: string): DDResizable { this.temporalRect = this.getChange(event, dir); this.applyChange(); const ev = DDUtils.initEvent(event, { type: 'resize', target: this.el }); @@ -143,22 +163,24 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt this.option.resize(ev, this.ui()); } this.triggerEvent('resize', ev); + return this; } - protected resizeStop(event: MouseEvent) { + protected resizeStop(event: MouseEvent): DDResizable { const ev = DDUtils.initEvent(event, { type: 'resizestop', target: this.el }); if (this.option.stop) { - this.option.stop(ev, this.ui()); + 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(); - this.startEvent = undefined; - this.originalRect = undefined; - this.temporalRect = undefined; + delete this.startEvent; + delete this.originalRect; + delete this.temporalRect; + return this; } - private setupHelper() { + private setupHelper(): DDResizable { this.elOriginStyle = DDResizable.originStyleProp.map(prop => this.el.style[prop]); this.parentOriginStylePosition = this.el.parentElement.style.position; if (window.getComputedStyle(this.el.parentElement).position.match(/static/)) { @@ -167,16 +189,20 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt this.el.style.position = this.option.basePosition || 'absolute'; // or 'fixed' this.el.style.opacity = '0.8'; this.el.style.zIndex = '1000'; + return this; } - private cleanHelper() { + + private cleanHelper(): DDResizable { DDResizable.originStyleProp.forEach(prop => { this.el.style[prop] = this.elOriginStyle[prop] || null; }); this.el.parentElement.style.position = this.parentOriginStylePosition || null; + return this; } - private getChange(event: MouseEvent, dir: string) { + + private getChange(event: MouseEvent, dir: string): Rect { const oEvent = this.startEvent; - const newRect = { + const newRect = { // Note: originalRect is a complex object, not a simple Rect, so copy out. width: this.originalRect.width, height: this.originalRect.height, left: this.originalRect.left, @@ -215,7 +241,7 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt return newRect; } - private getReShapeSize(oWidth, oHeight) { + private getReShapeSize(oWidth: number, oHeight: number): Size { const maxWidth = this.option.maxWidth || oWidth; const minWidth = this.option.minWidth || oWidth; const maxHeight = this.option.maxHeight || oHeight; @@ -225,7 +251,7 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt return { width, height }; } - private applyChange() { + private applyChange(): DDResizable { let containmentRect = { left: 0, top: 0, width: 0, height: 0 }; if (this.el.style.position === 'absolute') { const containmentEl = this.el.parentElement; @@ -236,48 +262,41 @@ export class DDResizable extends DDBaseImplement implements HTMLElementExtendOpt const value = this.temporalRect[key]; this.el.style[key] = value - containmentRect[key] + 'px'; }); + return this; } - protected removeHandlers() { + protected removeHandlers(): DDResizable { this.handlers.forEach(handle => handle.destroy()); - this.handlers = undefined; + delete this.handlers; + return this; } - destroy() { - this.removeHandlers(); - if (this.option.autoHide) { - this.el.removeEventListener('mouseover', this.showHandlers); - this.el.removeEventListener('mouseout', this.hideHandlers); - } - this.el.classList.remove('ui-resizable'); - this.el = undefined; - super.destroy(); - } - - ui = () => { + private ui = (): DDUIData => { const containmentEl = this.el.parentElement; const containmentRect = containmentEl.getBoundingClientRect(); const rect = this.temporalRect || this.originalRect; 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: { + originalPosition: { // The position represented as { left, top } before the resizable is resized left: this.originalRect.left - containmentRect.left, top: this.originalRect.top - containmentRect.top - }, // The position represented as { left, top } before the resizable is resized - originalSize: { + }, + originalSize: { // The size represented as { width, height } before the resizable is resized width: this.originalRect.width, height: this.originalRect.height - },// The size represented as { width, height } before the resizable is resized - position: { - left: rect.left - containmentRect.left, - top: rect.top - containmentRect.top - }, // The current position represented as { left, top } - size: { - width: rect.width, - height: rect.height - } // The current size represented as { width, height } + } + */ }; } } diff --git a/src/dragdrop/dd-utils.ts b/src/dragdrop/dd-utils.ts index 039674ae7..70c3a212d 100644 --- a/src/dragdrop/dd-utils.ts +++ b/src/dragdrop/dd-utils.ts @@ -5,8 +5,10 @@ * (c) 2020 rhlin, Alain Dumesny * gridstack.js may be freely distributed under the MIT license. */ + export class DDUtils { - static isEventSupportPassiveOption = ((() => { + + public static isEventSupportPassiveOption = ((() => { let supportsPassive = false; let passiveTest = () => { // do nothing @@ -21,13 +23,13 @@ export class DDUtils { return supportsPassive; })()); - static clone(el: HTMLElement): HTMLElement { + public static clone(el: HTMLElement): HTMLElement { const node = el.cloneNode(true) as HTMLElement; node.removeAttribute('id'); return node; } - static appendTo(el: HTMLElement, parent: string | HTMLElement | Node) { + public static appendTo(el: HTMLElement, parent: string | HTMLElement | Node): void { let parentNode: HTMLElement; if (typeof parent === 'string') { parentNode = document.querySelector(parent as string); @@ -38,13 +40,14 @@ export class DDUtils { parentNode.append(el); } } - static setPositionRelative(el) { + + public static setPositionRelative(el): void { if (!(/^(?:r|a|f)/).test(window.getComputedStyle(el).position)) { el.style.position = "relative"; } } - static addElStyles(el: HTMLElement, styles: { [prop: string]: string | string[] }) { + public static addElStyles(el: HTMLElement, styles: { [prop: string]: string | string[] }): void { if (styles instanceof Object) { for (const s in styles) { if (styles.hasOwnProperty(s)) { @@ -60,14 +63,8 @@ export class DDUtils { } } } - static copyProps(dst, src, props) { - for (let i = 0; i < props.length; i++) { - const p = props[i]; - dst[p] = src[p]; - } - } - static initEvent(e: DragEvent | MouseEvent, info: { type: string; target?: EventTarget }) { + public static initEvent(e: DragEvent | MouseEvent, info: { type: string; target?: EventTarget }): T { const kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(','); const ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY'.split(','); const evt = { type: info.type }; @@ -88,4 +85,11 @@ export class DDUtils { DDUtils.copyProps(evt, obj, Object.keys(obj)); return evt as unknown as T; } + + private static copyProps(dst: unknown, src: unknown, props: string[]): void { + for (let i = 0; i < props.length; i++) { + const p = props[i]; + dst[p] = src[p]; + } + } } diff --git a/src/dragdrop/gridstack-dd-native.ts b/src/dragdrop/gridstack-dd-native.ts index 413feb020..ed8a26071 100644 --- a/src/dragdrop/gridstack-dd-native.ts +++ b/src/dragdrop/gridstack-dd-native.ts @@ -123,6 +123,7 @@ export class GridStackDDNative extends GridStackDD { dEl.off(name); return this; } + private getGridStackDDElement(el: GridStackElement): DDElement { let dEl; if (typeof el === 'string') { diff --git a/src/gridstack.ts b/src/gridstack.ts index f884b7453..ee37e013b 100644 --- a/src/gridstack.ts +++ b/src/gridstack.ts @@ -8,7 +8,7 @@ import { GridStackEngine } from './gridstack-engine'; import { obsoleteOpts, obsoleteOptsDel, obsoleteAttr, obsolete, Utils, HeightData } from './utils'; -import { GridItemHTMLElement, GridStackWidget, GridStackNode, GridStackOptions, numberOrString, ColumnOptions } from './types'; +import { GridItemHTMLElement, GridStackWidget, GridStackNode, GridStackOptions, numberOrString, ColumnOptions, DDUIData } from './types'; import { GridStackDD } from './gridstack-dd'; // export all dependent file as well to make it easier for users to just import the main file @@ -533,7 +533,7 @@ export class GridStack { this.opts.column = this.engine.column = column; // update the items now - see if the dom order nodes should be passed instead (else default to current list) - let domNodes: GridStackNode[] = undefined; // explicitly leave not defined + let domNodes: GridStackNode[]; if (column === 1 && this.opts.oneColumnModeDomSort) { domNodes = []; this.getGridItems().forEach(el => { @@ -1227,18 +1227,18 @@ export class GridStack { /** @internal prepares the element for drag&drop **/ private _prepareDragDropByNode(node: GridStackNode): GridStack { // check for disabled grid first - if (this.opts.staticGrid || node.locked) { + if (this.opts.staticGrid || node.locked || + ((node.noMove || this.opts.disableDrag) && (node.noResize || this.opts.disableResize))) { if (node._initDD) { this.dd.remove(node.el); // nukes everything instead of just disable, will add some styles back next delete node._initDD; } node.el.classList.add('ui-draggable-disabled', 'ui-resizable-disabled'); // add styles one might depend on #1435 - return; + return this; } - // check if init already done or not needed (static/disabled) - if (node._initDD || this.opts.staticGrid || - ((node.noMove || this.opts.disableDrag) && (node.noResize || this.opts.disableResize))) { - return; + // check if init already done + if (node._initDD) { + return this; } // remove our style that look like D&D @@ -1250,8 +1250,8 @@ export class GridStack { let el = node.el; /** called when item starts moving/resizing */ - let onStartMoving = (event, ui) => { - let target: HTMLElement = event.target; + let onStartMoving = (event: Event, ui: DDUIData): void => { + let target = event.target as HTMLElement; // trigger any 'dragstart' / 'resizestart' manually if (this._gsEventHandler[event.type]) { @@ -1279,7 +1279,7 @@ export class GridStack { } /** called when item is being dragged/resized */ - let dragOrResize = (event: Event, ui) => { + let dragOrResize = (event: Event, ui: DDUIData): void => { let x = Math.round(ui.position.left / cellWidth); let y = Math.floor((ui.position.top + cellHeight / 2) / cellHeight); let width; @@ -1340,7 +1340,7 @@ export class GridStack { } /** called when the item stops moving/resizing */ - let onEndMoving = (event: Event) => { + let onEndMoving = (event: Event): void => { if (this.placeholder.parentNode === this.el) { this.placeholder.remove(); } diff --git a/src/jq/gridstack-dd-jqueryui.ts b/src/jq/gridstack-dd-jqueryui.ts index 5ee48714d..74d7914b5 100644 --- a/src/jq/gridstack-dd-jqueryui.ts +++ b/src/jq/gridstack-dd-jqueryui.ts @@ -38,11 +38,15 @@ export class GridStackDDJQueryUI extends GridStackDD { $el.resizable(opts, key, value); } else { let handles = $el.data('gs-resize-handles') ? $el.data('gs-resize-handles') : this.grid.opts.resizable.handles; - $el.resizable({...this.grid.opts.resizable, ...{handles: handles}, ...{ // was using $.extend() - start: opts.start, // || function() {}, - stop: opts.stop, // || function() {}, - resize: opts.resize // || function() {} - }}); + $el.resizable({ + ...this.grid.opts.resizable, + ...{ handles: handles }, + ...{ + start: opts.start, // || function() {}, + stop: opts.stop, // || function() {}, + resize: opts.resize // || function() {} + } + }); } return this; } diff --git a/src/types.ts b/src/types.ts index 0a0bdef78..35aec4135 100644 --- a/src/types.ts +++ b/src/types.ts @@ -253,6 +253,30 @@ export interface DDDragInOpt extends DDDragOpt { helper?: string | ((event: Event) => HTMLElement); } +export interface Size { + width: number; + height: number; +} +export interface Position { + 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[]; + */ +} + /** * internal descriptions describing the items in the grid */ diff --git a/webpack.config.js b/webpack.config.js index 742882531..374736e18 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,7 +8,7 @@ module.exports = { }, mode: 'production', // production vs development devtool: 'source-map', - // devtool: 'use eval-source-map', // for best (large .js) debugging. see https://survivejs.com/webpack/building/source-maps/ + // devtool: 'eval-source-map', // for best (large .js) debugging. see https://survivejs.com/webpack/building/source-maps/ module: { rules: [ { @@ -30,6 +30,6 @@ module.exports = { path: path.resolve(__dirname, 'dist'), library: 'GridStack', libraryExport: 'GridStack', - libraryTarget: 'umd', // "var" | "assign" | "this" | "window" | "self" | "global" | "commonjs" | "commonjs2" | "commonjs-module" | "amd" | "amd-require" | "umd" | "umd2" | "jsonp" | "system" + libraryTarget: 'umd', // var|assign|this|window|self|global|commonjs|commonjs2|commonjs-module|amd|amd-require|umd|umd2|jsonp|system } };