diff --git a/deleteDiscordMessages.user.js b/deleteDiscordMessages.user.js index e31efd9a..f40f4925 100644 --- a/deleteDiscordMessages.user.js +++ b/deleteDiscordMessages.user.js @@ -1,10 +1,10 @@ // ==UserScript== // @name Undiscord // @description Delete all messages in a Discord channel or DM (Bulk deletion) -// @version 5.1.1 +// @version 5.2.0 // @author victornpb // @homepageURL https://github.com/victornpb/undiscord -// @supportURL https://github.com/victornpb/undiscord/issues +// @supportURL https://github.com/victornpb/undiscord/discussions // @match https://*.discord.com/app // @match https://*.discord.com/channels/* // @match https://*.discord.com/login @@ -19,7 +19,7 @@ 'use strict'; /* rollup-plugin-baked-env */ - const VERSION = "5.1.1"; + const VERSION = "5.2.0"; var themeCss = (` /* undiscord window */ @@ -99,7 +99,7 @@ #undicord-btn.running { color: var(--button-danger-background) !important; } #undicord-btn.running progress { display: block; } /**** Undiscord Interface ****/ -#undiscord { position: fixed; z-index: 99; top: 44px; right: 10px; display: flex; flex-direction: column; width: 800px; height: 80vh; min-width: 610px; max-width: 100vw; min-height: 448px; max-height: 100vh; color: var(--text-normal); border-radius: 4px; background-color: var(--background-secondary); box-shadow: var(--elevation-stroke), var(--elevation-high); will-change: top, left, width, height; } +#undiscord { position: fixed; z-index: 100; top: 58px; right: 10px; display: flex; flex-direction: column; width: 800px; height: 80vh; min-width: 610px; max-width: 100vw; min-height: 448px; max-height: 100vh; color: var(--text-normal); border-radius: 4px; background-color: var(--background-secondary); box-shadow: var(--elevation-stroke), var(--elevation-high); will-change: top, left, width, height; } #undiscord .header .icon { cursor: pointer; } #undiscord .window-body { height: calc(100% - 48px); } #undiscord .sidebar { overflow: hidden scroll; overflow-y: auto; width: 270px; min-width: 250px; height: 100%; max-height: 100%; padding: 8px; background: var(--background-secondary); } @@ -121,6 +121,20 @@ #undiscord progress { height: 8px; margin-top: 4px; flex-grow: 1; } #undiscord .importJson { display: flex; flex-direction: row; } #undiscord .importJson button { margin-left: 5px; width: fit-content; } +`); + + var dragCss = (` +[name^="grab-"] { position: absolute; --size: 6px; --corner-size: 16px; --offset: -1px; z-index: 9; } +[name^="grab-"]:hover{ background: rgba(128,128,128,0.1); } +[name="grab-t"] { top: 0px; left: var(--corner-size); right: var(--corner-size); height: var(--size); margin-top: var(--offset); cursor: ns-resize; } +[name="grab-r"] { top: var(--corner-size); bottom: var(--corner-size); right: 0px; width: var(--size); margin-right: var(--offset); + cursor: ew-resize; } +[name="grab-b"] { bottom: 0px; left: var(--corner-size); right: var(--corner-size); height: var(--size); margin-bottom: var(--offset); cursor: ns-resize; } +[name="grab-l"] { top: var(--corner-size); bottom: var(--corner-size); left: 0px; width: var(--size); margin-left: var(--offset); cursor: ew-resize; } +[name="grab-tl"] { top: 0px; left: 0px; width: var(--corner-size); height: var(--corner-size); margin-top: var(--offset); margin-left: var(--offset); cursor: nwse-resize; } +[name="grab-tr"] { top: 0px; right: 0px; width: var(--corner-size); height: var(--corner-size); margin-top: var(--offset); margin-right: var(--offset); cursor: nesw-resize; } +[name="grab-br"] { bottom: 0px; right: 0px; width: var(--corner-size); height: var(--corner-size); margin-bottom: var(--offset); margin-right: var(--offset); cursor: nwse-resize; } +[name="grab-bl"] { bottom: 0px; left: 0px; width: var(--corner-size); height: var(--corner-size); margin-bottom: var(--offset); margin-left: var(--offset); cursor: nesw-resize; } `); var buttonHtml = (` @@ -807,124 +821,144 @@ } } - class Drag { - /** - * Make an element draggable/resizable - * @param {Element} targetElm The element that will be dragged/resized - * @param {Element} handleElm The element that will listen to events (handdle/grabber) - * @param {object} [options] Options - * @param {string} [options.mode="move"] Define the type of operation (move/resize) - * @param {number} [options.minWidth=200] Minimum width allowed to resize - * @param {number} [options.maxWidth=Infinity] Maximum width allowed to resize - * @param {number} [options.minHeight=100] Maximum height allowed to resize - * @param {number} [options.maxHeight=Infinity] Maximum height allowed to resize - * @param {string} [options.draggingClass="drag"] Class added to targetElm while being dragged - * @param {boolean} [options.useMouseEvents=true] Use mouse events - * @param {boolean} [options.useTouchEvents=true] Use touch events - * - * @author Victor N. wwww.vitim.us - */ - constructor(targetElm, handleElm, options) { - this.options = Object.assign({ - mode: 'move', + const MOVE = 0; + const RESIZE_T = 1; + const RESIZE_B = 2; + const RESIZE_L = 4; + const RESIZE_R = 8; + const RESIZE_TL = RESIZE_T + RESIZE_L; + const RESIZE_TR = RESIZE_T + RESIZE_R; + const RESIZE_BL = RESIZE_B + RESIZE_L; + const RESIZE_BR = RESIZE_B + RESIZE_R; + /** + * Make an element draggable/resizable + * @author Victor N. wwww.vitim.us + */ + class DragResize { + constructor({ elm, moveHandle, options }) { + this.options = defaultArgs({ + enabledDrag: true, + enabledResize: true, minWidth: 200, maxWidth: Infinity, minHeight: 100, maxHeight: Infinity, - xAxis: true, - yAxis: true, - + dragAllowX: true, + dragAllowY: true, + resizeAllowX: true, + resizeAllowY: true, draggingClass: 'drag', - useMouseEvents: true, useTouchEvents: true, + createHandlers: true, }, options); + Object.assign(this, options); + options = undefined; + + elm.style.position = 'fixed'; + + this.drag_m = new Draggable(elm, moveHandle, MOVE, this.options); + + if (this.options.createHandlers) { + this.el_t = createElement('div', { name: 'grab-t' }, elm); + this.drag_t = new Draggable(elm, this.el_t, RESIZE_T, this.options); + this.el_r = createElement('div', { name: 'grab-r' }, elm); + this.drag_r = new Draggable(elm, this.el_r, RESIZE_R, this.options); + this.el_b = createElement('div', { name: 'grab-b' }, elm); + this.drag_b = new Draggable(elm, this.el_b, RESIZE_B, this.options); + this.el_l = createElement('div', { name: 'grab-l' }, elm); + this.drag_l = new Draggable(elm, this.el_l, RESIZE_L, this.options); + this.el_tl = createElement('div', { name: 'grab-tl' }, elm); + this.drag_tl = new Draggable(elm, this.el_tl, RESIZE_TL, this.options); + this.el_tr = createElement('div', { name: 'grab-tr' }, elm); + this.drag_tr = new Draggable(elm, this.el_tr, RESIZE_TR, this.options); + this.el_br = createElement('div', { name: 'grab-br' }, elm); + this.drag_br = new Draggable(elm, this.el_br, RESIZE_BR, this.options); + this.el_bl = createElement('div', { name: 'grab-bl' }, elm); + this.drag_bl = new Draggable(elm, this.el_bl, RESIZE_BL, this.options); + } + } + } - // Public properties - this.minWidth = this.options.minWidth; - this.maxWidth = this.options.maxWidth; - this.minHeight = this.options.minHeight; - this.maxHeight = this.options.maxHeight; - this.xAxis = this.options.xAxis; - this.yAxis = this.options.yAxis; - this.draggingClass = this.options.draggingClass; + class Draggable { + constructor(targetElm, handleElm, op, options) { + Object.assign(this, options); + options = undefined; - /** @private */ this._targetElm = targetElm; - /** @private */ this._handleElm = handleElm; + let vw = window.innerWidth; + let vh = window.innerHeight; + let initialX, initialY, initialT, initialL, initialW, initialH; + + const clamp = (value, min, max) => value < min ? min : value > max ? max : value; + const moveOp = (x, y) => { - let l = x - offLeft; - if (x - offLeft < 0) l = 0; //offscreen <- - else if (x - offRight > vw) l = vw - this._targetElm.clientWidth; //offscreen -> - let t = y - offTop; - if (y - offTop < 0) t = 0; //offscreen /\ - else if (y - offBottom > vh) t = vh - this._targetElm.clientHeight; //offscreen \/ - - if(this.xAxis) this._targetElm.style.left = `${l}px`; - if(this.yAxis) this._targetElm.style.top = `${t}px`; - // NOTE: profilling on chrome translate wasn't faster than top/left as expected. And it also permanently creates a new layer, increasing vram usage. - // this._targetElm.style.transform = `translate(${l}px, ${t}px)`; + const deltaX = (x - initialX); + const deltaY = (y - initialY); + const t = clamp(initialT + deltaY, 0, vh - initialH); + const l = clamp(initialL + deltaX, 0, vw - initialW); + this._targetElm.style.top = t + 'px'; + this._targetElm.style.left = l + 'px'; }; const resizeOp = (x, y) => { - let w = x - this._targetElm.offsetLeft - offRight; - if (x - offRight > vw) w = Math.min(vw - this._targetElm.offsetLeft, this.maxWidth); //offscreen -> - else if (x - offRight - this._targetElm.offsetLeft > this.maxWidth) w = this.maxWidth; //max width - else if (x - offRight - this._targetElm.offsetLeft < this.minWidth) w = this.minWidth; //min width - let h = y - this._targetElm.offsetTop - offBottom; - if (y - offBottom > vh) h = Math.min(vh - this._targetElm.offsetTop, this.maxHeight); //offscreen \/ - else if (y - offBottom - this._targetElm.offsetTop > this.maxHeight) h = this.maxHeight; //max height - else if (y - offBottom - this._targetElm.offsetTop < this.minHeight) h = this.minHeight; //min height - - if(this.xAxis) this._targetElm.style.width = `${w}px`; - if(this.yAxis) this._targetElm.style.height = `${h}px`; + x = clamp(x, 0, vw); + y = clamp(y, 0, vh); + const deltaX = (x - initialX); + const deltaY = (y - initialY); + const resizeDirX = (op & RESIZE_L) ? -1 : 1; + const resizeDirY = (op & RESIZE_T) ? -1 : 1; + const deltaXMax = (this.maxWidth - initialW); + const deltaXMin = (this.minWidth - initialW); + const deltaYMax = (this.maxHeight - initialH); + const deltaYMin = (this.minHeight - initialH); + const t = initialT + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax) * resizeDirY; + const l = initialL + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax) * resizeDirX; + const w = initialW + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax); + const h = initialH + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax); + if (op & RESIZE_T) { // resize ↑ + this._targetElm.style.top = t + 'px'; + this._targetElm.style.height = h + 'px'; + } + if (op & RESIZE_B) { // resize ↓ + this._targetElm.style.height = h + 'px'; + } + if (op & RESIZE_L) { // resize ← + this._targetElm.style.left = l + 'px'; + this._targetElm.style.width = w + 'px'; + } + if (op & RESIZE_R) { // resize → + this._targetElm.style.width = w + 'px'; + } }; - // define which operation is performed on drag - const operation = this.options.mode === 'move' ? moveOp : resizeOp; - - // offset from the initial click to the target boundaries - let offTop, offLeft, offBottom, offRight; - - let vw = window.innerWidth; - let vh = window.innerHeight; - + let operation = op === MOVE ? moveOp : resizeOp; function dragStartHandler(e) { const touch = e.type === 'touchstart'; - if ((e.buttons === 1 || e.which === 1) || touch) { e.preventDefault(); - const x = touch ? e.touches[0].clientX : e.clientX; const y = touch ? e.touches[0].clientY : e.clientY; - - const targetOffset = this._targetElm.getBoundingClientRect(); - - //offset from the click to the top-left corner of the target (drag) - offTop = y - targetOffset.y; - offLeft = x - targetOffset.x; - //offset from the click to the bottom-right corner of the target (resize) - offBottom = y - (targetOffset.y + targetOffset.height); - offRight = x - (targetOffset.x + targetOffset.width); - + initialX = x; + initialY = y; vw = window.innerWidth; vh = window.innerHeight; - - if (this.options.useMouseEvents) { + initialT = this._targetElm.offsetTop; + initialL = this._targetElm.offsetLeft; + initialW = this._targetElm.clientWidth; + initialH = this._targetElm.clientHeight; + if (this.useMouseEvents) { document.addEventListener('mousemove', this._dragMoveHandler); document.addEventListener('mouseup', this._dragEndHandler); } - if (this.options.useTouchEvents) { - document.addEventListener('touchmove', this._dragMoveHandler, { - passive: false, - }); + if (this.useTouchEvents) { + document.addEventListener('touchmove', this._dragMoveHandler, { passive: false }); document.addEventListener('touchend', this._dragEndHandler); } - this._targetElm.classList.add(this.draggingClass); } } @@ -932,74 +966,61 @@ function dragMoveHandler(e) { e.preventDefault(); let x, y; - const touch = e.type === 'touchmove'; if (touch) { const t = e.touches[0]; x = t.clientX; y = t.clientY; } else { //mouse - // If the button is not down, dispatch a "fake" mouse up event, to stop listening to mousemove // This happens when the mouseup is not captured (outside the browser) if ((e.buttons || e.which) !== 1) { this._dragEndHandler(); return; } - x = e.clientX; y = e.clientY; } - + // perform drag / resize operation operation(x, y); } function dragEndHandler(e) { - if (this.options.useMouseEvents) { + if (this.useMouseEvents) { document.removeEventListener('mousemove', this._dragMoveHandler); document.removeEventListener('mouseup', this._dragEndHandler); } - if (this.options.useTouchEvents) { + if (this.useTouchEvents) { document.removeEventListener('touchmove', this._dragMoveHandler); document.removeEventListener('touchend', this._dragEndHandler); } this._targetElm.classList.remove(this.draggingClass); } - // We need to bind the handlers to this instance and expose them to methods enable and destroy - /** @private */ + // We need to bind the handlers to this instance this._dragStartHandler = dragStartHandler.bind(this); - /** @private */ this._dragMoveHandler = dragMoveHandler.bind(this); - /** @private */ this._dragEndHandler = dragEndHandler.bind(this); this.enable(); } - /** - * Turn on the drag and drop of the instancea - * @memberOf Drag - */ + /** Turn on the drag and drop of the instance */ enable() { - // this.destroy(); // prevent events from getting binded twice - if (this.options.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler); - if (this.options.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false }); + this.destroy(); // prevent events from getting binded twice + if (this.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler); + if (this.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false }); } - /** - * Teardown all events bound to the document and elements - * You can resurrect this instance by calling enable() - * @memberOf Drag - */ + + /** Teardown all events bound to the document and elements. You can resurrect this instance by calling enable() */ destroy() { this._targetElm.classList.remove(this.draggingClass); - - if (this.options.useMouseEvents) { + if (this.useMouseEvents) { this._handleElm.removeEventListener('mousedown', this._dragStartHandler); document.removeEventListener('mousemove', this._dragMoveHandler); document.removeEventListener('mouseup', this._dragEndHandler); } - if (this.options.useTouchEvents) { + if (this.useTouchEvents) { this._handleElm.removeEventListener('touchstart', this._dragStartHandler); document.removeEventListener('touchmove', this._dragMoveHandler); document.removeEventListener('touchend', this._dragEndHandler); @@ -1007,6 +1028,25 @@ } } + function createElement(tag='div', attrs, parent) { + const elm = document.createElement(tag); + if (attrs) Object.entries(attrs).forEach(([k, v]) => elm.setAttribute(k, v)); + if (parent) parent.appendChild(elm); + return elm; + } + + function defaultArgs(defaults, options) { + function isObj(x) { return x !== null && typeof x === 'object'; } + function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } + if (isObj(options)) for (let prop in defaults) { + if (hasOwn(defaults, prop) && hasOwn(options, prop) && options[prop] !== undefined) { + if (isObj(defaults[prop])) defaultArgs(defaults[prop], options[prop]); + else defaults[prop] = options[prop]; + } + } + return defaults; + } + function createElm(html) { const temp = document.createElement('div'); temp.innerHTML = html; @@ -1138,6 +1178,7 @@ body.undiscord-pick-message.after [id^="message-content-"]:hover::after { insertCss(themeCss); insertCss(mainCss); + insertCss(dragCss); // create undiscord window const undiscordUI = replaceInterpolations(undiscordTemplate, { @@ -1149,8 +1190,7 @@ body.undiscord-pick-message.after [id^="message-content-"]:hover::after { document.body.appendChild(ui.undiscordWindow); // enable drag and resize on undiscord window - new Drag(ui.undiscordWindow, $('.header'), { mode: 'move' }); - new Drag(ui.undiscordWindow, $('.footer'), { mode: 'resize' }); + new DragResize({ elm: ui.undiscordWindow, moveHandle: $('.header') }); // create undiscord Trash icon ui.undiscordBtn = createElm(buttonHtml); diff --git a/package-lock.json b/package-lock.json index 8615bbae..a90db03e 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "undiscord", - "version": "5.1.1", + "version": "5.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "undiscord", - "version": "5.1.1", + "version": "5.2.0", "license": "MIT", "devDependencies": { "@rollup/plugin-json": "^6.0.0", diff --git a/package.json b/package.json index 68578773..d0f922ff 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "undiscord", "nameFull": "Undiscord", - "version": "5.1.1", + "version": "5.2.0", "description": "Delete all messages in a Discord channel or DM (Bulk deletion)", "userScript": { "namespace": "https://github.com/victornpb/deleteDiscordMessages", diff --git a/src/ui/drag.css b/src/ui/drag.css new file mode 100644 index 00000000..ac72b6d2 --- /dev/null +++ b/src/ui/drag.css @@ -0,0 +1,79 @@ +[name^="grab-"] { + position: absolute; + --size: 6px; + --corner-size: 16px; + --offset: -1px; + z-index: 9; +} +[name^="grab-"]:hover{ + background: rgba(128,128,128,0.1); +} + +[name="grab-t"] { + top: 0px; + left: var(--corner-size); + right: var(--corner-size); + height: var(--size); + margin-top: var(--offset); + cursor: ns-resize; +} +[name="grab-r"] { + top: var(--corner-size); + bottom: var(--corner-size); + right: 0px; + width: var(--size); + margin-right: var(--offset); + cursor: ew-resize; +} +[name="grab-b"] { + bottom: 0px; + left: var(--corner-size); + right: var(--corner-size); + height: var(--size); + margin-bottom: var(--offset); + cursor: ns-resize; +} +[name="grab-l"] { + top: var(--corner-size); + bottom: var(--corner-size); + left: 0px; + width: var(--size); + margin-left: var(--offset); + cursor: ew-resize; +} +[name="grab-tl"] { + top: 0px; + left: 0px; + width: var(--corner-size); + height: var(--corner-size); + margin-top: var(--offset); + margin-left: var(--offset); + cursor: nwse-resize; +} +[name="grab-tr"] { + top: 0px; + right: 0px; + width: var(--corner-size); + height: var(--corner-size); + margin-top: var(--offset); + margin-right: var(--offset); + cursor: nesw-resize; +} +[name="grab-br"] { + bottom: 0px; + right: 0px; + width: var(--corner-size); + height: var(--corner-size); + margin-bottom: var(--offset); + margin-right: var(--offset); + cursor: nwse-resize; +} +[name="grab-bl"] { + bottom: 0px; + left: 0px; + width: var(--corner-size); + height: var(--corner-size); + margin-bottom: var(--offset); + margin-left: var(--offset); + cursor: nesw-resize; +} \ No newline at end of file diff --git a/src/ui/main.css b/src/ui/main.css index e8694241..a083690b 100644 --- a/src/ui/main.css +++ b/src/ui/main.css @@ -29,8 +29,8 @@ /**** Undiscord Interface ****/ #undiscord { position: fixed; - z-index: 99; - top: 44px; + z-index: 100; + top: 58px; right: 10px; display: flex; flex-direction: column; diff --git a/src/undiscord-ui.js b/src/undiscord-ui.js index bc269b38..7ce3c956 100644 --- a/src/undiscord-ui.js +++ b/src/undiscord-ui.js @@ -4,6 +4,7 @@ import { VERSION } from 'process.env'; import themeCss from './ui/theme.css'; import mainCss from './ui/main.css'; +import dragCss from './ui/drag.css'; import buttonHtml from './ui/undiscord-button.html'; import undiscordTemplate from './ui/undiscord.html'; @@ -43,6 +44,7 @@ function initUI() { insertCss(themeCss); insertCss(mainCss); + insertCss(dragCss); // create undiscord window const undiscordUI = replaceInterpolations(undiscordTemplate, { @@ -54,8 +56,7 @@ function initUI() { document.body.appendChild(ui.undiscordWindow); // enable drag and resize on undiscord window - new Drag(ui.undiscordWindow, $('.header'), { mode: 'move' }); - new Drag(ui.undiscordWindow, $('.footer'), { mode: 'resize' }); + new Drag({ elm: ui.undiscordWindow, moveHandle: $('.header') }); // create undiscord Trash icon ui.undiscordBtn = createElm(buttonHtml); diff --git a/src/utils/drag.js b/src/utils/drag.js index e5ac6b7d..dce0c901 100644 --- a/src/utils/drag.js +++ b/src/utils/drag.js @@ -1,121 +1,141 @@ -export default class Drag { - /** - * Make an element draggable/resizable - * @param {Element} targetElm The element that will be dragged/resized - * @param {Element} handleElm The element that will listen to events (handdle/grabber) - * @param {object} [options] Options - * @param {string} [options.mode="move"] Define the type of operation (move/resize) - * @param {number} [options.minWidth=200] Minimum width allowed to resize - * @param {number} [options.maxWidth=Infinity] Maximum width allowed to resize - * @param {number} [options.minHeight=100] Maximum height allowed to resize - * @param {number} [options.maxHeight=Infinity] Maximum height allowed to resize - * @param {string} [options.draggingClass="drag"] Class added to targetElm while being dragged - * @param {boolean} [options.useMouseEvents=true] Use mouse events - * @param {boolean} [options.useTouchEvents=true] Use touch events - * - * @author Victor N. wwww.vitim.us - */ - constructor(targetElm, handleElm, options) { - this.options = Object.assign({ - mode: 'move', - +const MOVE = 0; +const RESIZE_T = 1; +const RESIZE_B = 2; +const RESIZE_L = 4; +const RESIZE_R = 8; +const RESIZE_TL = RESIZE_T + RESIZE_L; +const RESIZE_TR = RESIZE_T + RESIZE_R; +const RESIZE_BL = RESIZE_B + RESIZE_L; +const RESIZE_BR = RESIZE_B + RESIZE_R; + +/** + * Make an element draggable/resizable + * @author Victor N. wwww.vitim.us + */ +export default class DragResize { + constructor({ elm, moveHandle, options }) { + this.options = defaultArgs({ + enabledDrag: true, + enabledResize: true, minWidth: 200, maxWidth: Infinity, minHeight: 100, maxHeight: Infinity, - xAxis: true, - yAxis: true, - + dragAllowX: true, + dragAllowY: true, + resizeAllowX: true, + resizeAllowY: true, draggingClass: 'drag', - useMouseEvents: true, useTouchEvents: true, + createHandlers: true, }, options); + Object.assign(this, options); + options = undefined; + + elm.style.position = 'fixed'; + + this.drag_m = new Draggable(elm, moveHandle, MOVE, this.options); + + if (this.options.createHandlers) { + this.el_t = createElement('div', { name: 'grab-t' }, elm); + this.drag_t = new Draggable(elm, this.el_t, RESIZE_T, this.options); + this.el_r = createElement('div', { name: 'grab-r' }, elm); + this.drag_r = new Draggable(elm, this.el_r, RESIZE_R, this.options); + this.el_b = createElement('div', { name: 'grab-b' }, elm); + this.drag_b = new Draggable(elm, this.el_b, RESIZE_B, this.options); + this.el_l = createElement('div', { name: 'grab-l' }, elm); + this.drag_l = new Draggable(elm, this.el_l, RESIZE_L, this.options); + this.el_tl = createElement('div', { name: 'grab-tl' }, elm); + this.drag_tl = new Draggable(elm, this.el_tl, RESIZE_TL, this.options); + this.el_tr = createElement('div', { name: 'grab-tr' }, elm); + this.drag_tr = new Draggable(elm, this.el_tr, RESIZE_TR, this.options); + this.el_br = createElement('div', { name: 'grab-br' }, elm); + this.drag_br = new Draggable(elm, this.el_br, RESIZE_BR, this.options); + this.el_bl = createElement('div', { name: 'grab-bl' }, elm); + this.drag_bl = new Draggable(elm, this.el_bl, RESIZE_BL, this.options); + } + } +} - // Public properties - this.minWidth = this.options.minWidth; - this.maxWidth = this.options.maxWidth; - this.minHeight = this.options.minHeight; - this.maxHeight = this.options.maxHeight; - this.xAxis = this.options.xAxis; - this.yAxis = this.options.yAxis; - this.draggingClass = this.options.draggingClass; +class Draggable { + constructor(targetElm, handleElm, op, options) { + Object.assign(this, options); + options = undefined; - /** @private */ this._targetElm = targetElm; - /** @private */ this._handleElm = handleElm; + let vw = window.innerWidth; + let vh = window.innerHeight; + let initialX, initialY, initialT, initialL, initialW, initialH; + + const clamp = (value, min, max) => value < min ? min : value > max ? max : value; + const moveOp = (x, y) => { - let l = x - offLeft; - if (x - offLeft < 0) l = 0; //offscreen <- - else if (x - offRight > vw) l = vw - this._targetElm.clientWidth; //offscreen -> - let t = y - offTop; - if (y - offTop < 0) t = 0; //offscreen /\ - else if (y - offBottom > vh) t = vh - this._targetElm.clientHeight; //offscreen \/ - - if(this.xAxis) this._targetElm.style.left = `${l}px`; - if(this.yAxis) this._targetElm.style.top = `${t}px`; - // NOTE: profilling on chrome translate wasn't faster than top/left as expected. And it also permanently creates a new layer, increasing vram usage. - // this._targetElm.style.transform = `translate(${l}px, ${t}px)`; + const deltaX = (x - initialX); + const deltaY = (y - initialY); + const t = clamp(initialT + deltaY, 0, vh - initialH); + const l = clamp(initialL + deltaX, 0, vw - initialW); + this._targetElm.style.top = t + 'px'; + this._targetElm.style.left = l + 'px'; }; const resizeOp = (x, y) => { - let w = x - this._targetElm.offsetLeft - offRight; - if (x - offRight > vw) w = Math.min(vw - this._targetElm.offsetLeft, this.maxWidth); //offscreen -> - else if (x - offRight - this._targetElm.offsetLeft > this.maxWidth) w = this.maxWidth; //max width - else if (x - offRight - this._targetElm.offsetLeft < this.minWidth) w = this.minWidth; //min width - let h = y - this._targetElm.offsetTop - offBottom; - if (y - offBottom > vh) h = Math.min(vh - this._targetElm.offsetTop, this.maxHeight); //offscreen \/ - else if (y - offBottom - this._targetElm.offsetTop > this.maxHeight) h = this.maxHeight; //max height - else if (y - offBottom - this._targetElm.offsetTop < this.minHeight) h = this.minHeight; //min height - - if(this.xAxis) this._targetElm.style.width = `${w}px`; - if(this.yAxis) this._targetElm.style.height = `${h}px`; + x = clamp(x, 0, vw); + y = clamp(y, 0, vh); + const deltaX = (x - initialX); + const deltaY = (y - initialY); + const resizeDirX = (op & RESIZE_L) ? -1 : 1; + const resizeDirY = (op & RESIZE_T) ? -1 : 1; + const deltaXMax = (this.maxWidth - initialW); + const deltaXMin = (this.minWidth - initialW); + const deltaYMax = (this.maxHeight - initialH); + const deltaYMin = (this.minHeight - initialH); + const t = initialT + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax) * resizeDirY; + const l = initialL + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax) * resizeDirX; + const w = initialW + clamp(deltaX * resizeDirX, deltaXMin, deltaXMax); + const h = initialH + clamp(deltaY * resizeDirY, deltaYMin, deltaYMax); + if (op & RESIZE_T) { // resize ↑ + this._targetElm.style.top = t + 'px'; + this._targetElm.style.height = h + 'px'; + } + if (op & RESIZE_B) { // resize ↓ + this._targetElm.style.height = h + 'px'; + } + if (op & RESIZE_L) { // resize ← + this._targetElm.style.left = l + 'px'; + this._targetElm.style.width = w + 'px'; + } + if (op & RESIZE_R) { // resize → + this._targetElm.style.width = w + 'px'; + } }; - // define which operation is performed on drag - const operation = this.options.mode === 'move' ? moveOp : resizeOp; - - // offset from the initial click to the target boundaries - let offTop, offLeft, offBottom, offRight; - - let vw = window.innerWidth; - let vh = window.innerHeight; - + let operation = op === MOVE ? moveOp : resizeOp; function dragStartHandler(e) { const touch = e.type === 'touchstart'; - if ((e.buttons === 1 || e.which === 1) || touch) { e.preventDefault(); - const x = touch ? e.touches[0].clientX : e.clientX; const y = touch ? e.touches[0].clientY : e.clientY; - - const targetOffset = this._targetElm.getBoundingClientRect(); - - //offset from the click to the top-left corner of the target (drag) - offTop = y - targetOffset.y; - offLeft = x - targetOffset.x; - //offset from the click to the bottom-right corner of the target (resize) - offBottom = y - (targetOffset.y + targetOffset.height); - offRight = x - (targetOffset.x + targetOffset.width); - + initialX = x; + initialY = y; vw = window.innerWidth; vh = window.innerHeight; - - if (this.options.useMouseEvents) { + initialT = this._targetElm.offsetTop; + initialL = this._targetElm.offsetLeft; + initialW = this._targetElm.clientWidth; + initialH = this._targetElm.clientHeight; + if (this.useMouseEvents) { document.addEventListener('mousemove', this._dragMoveHandler); document.addEventListener('mouseup', this._dragEndHandler); } - if (this.options.useTouchEvents) { - document.addEventListener('touchmove', this._dragMoveHandler, { - passive: false, - }); + if (this.useTouchEvents) { + document.addEventListener('touchmove', this._dragMoveHandler, { passive: false }); document.addEventListener('touchend', this._dragEndHandler); } - this._targetElm.classList.add(this.draggingClass); } } @@ -123,77 +143,83 @@ export default class Drag { function dragMoveHandler(e) { e.preventDefault(); let x, y; - const touch = e.type === 'touchmove'; if (touch) { const t = e.touches[0]; x = t.clientX; y = t.clientY; } else { //mouse - // If the button is not down, dispatch a "fake" mouse up event, to stop listening to mousemove // This happens when the mouseup is not captured (outside the browser) if ((e.buttons || e.which) !== 1) { this._dragEndHandler(); return; } - x = e.clientX; y = e.clientY; } - + // perform drag / resize operation operation(x, y); } function dragEndHandler(e) { - if (this.options.useMouseEvents) { + if (this.useMouseEvents) { document.removeEventListener('mousemove', this._dragMoveHandler); document.removeEventListener('mouseup', this._dragEndHandler); } - if (this.options.useTouchEvents) { + if (this.useTouchEvents) { document.removeEventListener('touchmove', this._dragMoveHandler); document.removeEventListener('touchend', this._dragEndHandler); } this._targetElm.classList.remove(this.draggingClass); } - // We need to bind the handlers to this instance and expose them to methods enable and destroy - /** @private */ + // We need to bind the handlers to this instance this._dragStartHandler = dragStartHandler.bind(this); - /** @private */ this._dragMoveHandler = dragMoveHandler.bind(this); - /** @private */ this._dragEndHandler = dragEndHandler.bind(this); this.enable(); } - /** - * Turn on the drag and drop of the instancea - * @memberOf Drag - */ + /** Turn on the drag and drop of the instance */ enable() { - // this.destroy(); // prevent events from getting binded twice - if (this.options.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler); - if (this.options.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false }); + this.destroy(); // prevent events from getting binded twice + if (this.useMouseEvents) this._handleElm.addEventListener('mousedown', this._dragStartHandler); + if (this.useTouchEvents) this._handleElm.addEventListener('touchstart', this._dragStartHandler, { passive: false }); } - /** - * Teardown all events bound to the document and elements - * You can resurrect this instance by calling enable() - * @memberOf Drag - */ + + /** Teardown all events bound to the document and elements. You can resurrect this instance by calling enable() */ destroy() { this._targetElm.classList.remove(this.draggingClass); - - if (this.options.useMouseEvents) { + if (this.useMouseEvents) { this._handleElm.removeEventListener('mousedown', this._dragStartHandler); document.removeEventListener('mousemove', this._dragMoveHandler); document.removeEventListener('mouseup', this._dragEndHandler); } - if (this.options.useTouchEvents) { + if (this.useTouchEvents) { this._handleElm.removeEventListener('touchstart', this._dragStartHandler); document.removeEventListener('touchmove', this._dragMoveHandler); document.removeEventListener('touchend', this._dragEndHandler); } } +} + +function createElement(tag='div', attrs, parent) { + const elm = document.createElement(tag); + if (attrs) Object.entries(attrs).forEach(([k, v]) => elm.setAttribute(k, v)); + if (parent) parent.appendChild(elm); + return elm; +} + +function defaultArgs(defaults, options) { + function isObj(x) { return x !== null && typeof x === 'object'; } + function hasOwn(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } + if (isObj(options)) for (let prop in defaults) { + if (hasOwn(defaults, prop) && hasOwn(options, prop) && options[prop] !== undefined) { + if (isObj(defaults[prop])) defaultArgs(defaults[prop], options[prop]); + else defaults[prop] = options[prop]; + } + } + return defaults; } \ No newline at end of file