From 732dc952de5f020cc1e4d62a8c7bf4e463e10eec Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 27 Mar 2024 18:34:39 +0100 Subject: [PATCH] refactor(frontend): bring tether-drop into vendor folder to build from source --- package.json | 1 - public/vendor/tether-drop/index.js | 495 +++++++++++++++++++++++ public/vendor/tether-drop/tetherUtils.js | 378 +++++++++++++++++ yarn.lock | 17 - 4 files changed, 873 insertions(+), 18 deletions(-) create mode 100644 public/vendor/tether-drop/index.js create mode 100644 public/vendor/tether-drop/tetherUtils.js diff --git a/package.json b/package.json index 1ba919abba3..ab40063a93e 100644 --- a/package.json +++ b/package.json @@ -392,7 +392,6 @@ "symbol-observable": "4.0.0", "systemjs": "6.15.1", "systemjs-cjs-extra": "0.2.1", - "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.6.0", "tslib": "2.8.1", "tween-functions": "^1.2.0", diff --git a/public/vendor/tether-drop/index.js b/public/vendor/tether-drop/index.js new file mode 100644 index 00000000000..486fe76c0d6 --- /dev/null +++ b/public/vendor/tether-drop/index.js @@ -0,0 +1,495 @@ +import { + extend, + addClass, + removeClass, + hasClass, + Evented, +} from "./tetherUtils"; + +function sortAttach(str) { + let [first, second] = str.split(" "); + if (["left", "right"].indexOf(first) >= 0) { + [first, second] = [second, first]; + } + return [first, second].join(" "); +} + +function removeFromArray(arr, item) { + let index; + let results = []; + while ((index = arr.indexOf(item)) !== -1) { + results.push(arr.splice(index, 1)); + } + return results; +} + +let clickEvents = ["click"]; +if ("ontouchstart" in document.documentElement) { + clickEvents.push("touchstart"); +} + +const transitionEndEvents = { + WebkitTransition: "webkitTransitionEnd", + MozTransition: "transitionend", + OTransition: "otransitionend", + transition: "transitionend", +}; + +let transitionEndEvent = ""; +for (let name in transitionEndEvents) { + if ({}.hasOwnProperty.call(transitionEndEvents, name)) { + let tempEl = document.createElement("p"); + if (typeof tempEl.style[name] !== "undefined") { + transitionEndEvent = transitionEndEvents[name]; + } + } +} + +const MIRROR_ATTACH = { + left: "right", + right: "left", + top: "bottom", + bottom: "top", + middle: "middle", + center: "center", +}; + +let allDrops = {}; + +// Drop can be included in external libraries. Calling createContext gives you a fresh +// copy of drop which won't interact with other copies on the page (beyond calling the document events). + +function createContext(options = {}) { + let drop = (...args) => new DropInstance(...args); + + extend(drop, { + createContext: createContext, + drops: [], + defaults: {}, + }); + + const defaultOptions = { + classPrefix: "drop", + defaults: { + position: "bottom left", + openOn: "click", + beforeClose: null, + constrainToScrollParent: true, + constrainToWindow: true, + classes: "", + remove: false, + openDelay: 0, + closeDelay: 50, + // inherited from openDelay and closeDelay if not explicitly defined + focusDelay: null, + blurDelay: null, + hoverOpenDelay: null, + hoverCloseDelay: null, + tetherOptions: {}, + }, + }; + + extend(drop, defaultOptions, options); + extend(drop.defaults, defaultOptions.defaults, options.defaults); + + if (typeof allDrops[drop.classPrefix] === "undefined") { + allDrops[drop.classPrefix] = []; + } + + drop.updateBodyClasses = () => { + // There is only one body, so despite the context concept, we still iterate through all + // drops which share our classPrefix. + + let anyOpen = false; + const drops = allDrops[drop.classPrefix]; + const len = drops.length; + for (let i = 0; i < len; ++i) { + if (drops[i].isOpened()) { + anyOpen = true; + break; + } + } + + if (anyOpen) { + addClass(document.body, `${drop.classPrefix}-open`); + } else { + removeClass(document.body, `${drop.classPrefix}-open`); + } + }; + + class DropInstance extends Evented { + constructor(opts) { + super(); + this.options = extend({}, drop.defaults, opts); + this.target = this.options.target; + + if (typeof this.target === "undefined") { + throw new Error("Drop Error: You must provide a target."); + } + + const dataPrefix = `data-${drop.classPrefix}`; + + const contentAttr = this.target.getAttribute(dataPrefix); + if (contentAttr && this.options.content == null) { + this.options.content = contentAttr; + } + + const attrsOverride = ["position", "openOn"]; + for (let i = 0; i < attrsOverride.length; ++i) { + const override = this.target.getAttribute( + `${dataPrefix}-${attrsOverride[i]}` + ); + if (override && this.options[attrsOverride[i]] == null) { + this.options[attrsOverride[i]] = override; + } + } + + if (this.options.classes && this.options.addTargetClasses !== false) { + addClass(this.target, this.options.classes); + } + + drop.drops.push(this); + allDrops[drop.classPrefix].push(this); + + this._boundEvents = []; + this.bindMethods(); + this.setupElements(); + this.setupEvents(); + this.setupTether(); + } + + _on(element, event, handler) { + this._boundEvents.push({ element, event, handler }); + element.addEventListener(event, handler); + } + + bindMethods() { + this.transitionEndHandler = this._transitionEndHandler.bind(this); + } + + setupElements() { + this.drop = document.createElement("div"); + addClass(this.drop, drop.classPrefix); + + if (this.options.classes) { + addClass(this.drop, this.options.classes); + } + + this.content = document.createElement("div"); + addClass(this.content, `${drop.classPrefix}-content`); + + if (typeof this.options.content === "function") { + const generateAndSetContent = () => { + // content function might return a string or an element + const contentElementOrHTML = this.options.content.call(this, this); + + if (typeof contentElementOrHTML === "string") { + this.content.innerHTML = contentElementOrHTML; + } else if (typeof contentElementOrHTML === "object") { + this.content.innerHTML = ""; + this.content.appendChild(contentElementOrHTML); + } else { + throw new Error( + "Drop Error: Content function should return a string or HTMLElement." + ); + } + }; + + this.on("beforeOpen", generateAndSetContent.bind(this)); + } else if (typeof this.options.content === "object") { + this.content.appendChild(this.options.content); + } else { + this.content.innerHTML = this.options.content; + } + + this.drop.appendChild(this.content); + } + + setupTether() { + // Tether expects two attachment points, one in the target element, one in the + // drop. We use a single one, and use the order as well, to allow us to put + // the drop on either side of any of the four corners. This magic converts between + // the two: + let dropAttach = this.options.position.split(" "); + dropAttach[0] = MIRROR_ATTACH[dropAttach[0]]; + dropAttach = dropAttach.join(" "); + + let constraints = []; + if (this.options.constrainToScrollParent) { + constraints.push({ + to: "scrollParent", + pin: "top, bottom", + attachment: "together none", + }); + } else { + // To get 'out of bounds' classes + constraints.push({ + to: "scrollParent", + }); + } + + if (this.options.constrainToWindow !== false) { + constraints.push({ + to: "window", + attachment: "together", + }); + } else { + // To get 'out of bounds' classes + constraints.push({ + to: "window", + }); + } + + const opts = { + element: this.drop, + target: this.target, + attachment: sortAttach(dropAttach), + targetAttachment: sortAttach(this.options.position), + classPrefix: drop.classPrefix, + offset: "0 0", + targetOffset: "0 0", + enabled: false, + constraints: constraints, + addTargetClasses: this.options.addTargetClasses, + }; + + if (this.options.tetherOptions !== false) { + this.tether = new Tether(extend({}, opts, this.options.tetherOptions)); + } + } + + setupEvents() { + if (!this.options.openOn) { + return; + } + + if (this.options.openOn === "always") { + setTimeout(this.open.bind(this)); + return; + } + + const events = this.options.openOn.split(" "); + + if (events.indexOf("click") >= 0) { + const openHandler = (event) => { + this.toggle(event); + event.preventDefault(); + }; + + const closeHandler = (event) => { + if (!this.isOpened()) { + return; + } + + // Clicking inside dropdown + if (event.target === this.drop || this.drop.contains(event.target)) { + return; + } + + // Clicking target + if ( + event.target === this.target || + this.target.contains(event.target) + ) { + return; + } + + this.close(event); + }; + + for (let i = 0; i < clickEvents.length; ++i) { + const clickEvent = clickEvents[i]; + this._on(this.target, clickEvent, openHandler); + this._on(document, clickEvent, closeHandler); + } + } + + let inTimeout = null; + let outTimeout = null; + + const inHandler = (event) => { + if (outTimeout !== null) { + clearTimeout(outTimeout); + } else { + inTimeout = setTimeout(() => { + this.open(event); + inTimeout = null; + }, (event.type === "focus" ? this.options.focusDelay : this.options.hoverOpenDelay) || this.options.openDelay); + } + }; + + const outHandler = (event) => { + if (inTimeout !== null) { + clearTimeout(inTimeout); + } else { + outTimeout = setTimeout(() => { + this.close(event); + outTimeout = null; + }, (event.type === "blur" ? this.options.blurDelay : this.options.hoverCloseDelay) || this.options.closeDelay); + } + }; + + if (events.indexOf("hover") >= 0) { + this._on(this.target, "mouseover", inHandler); + this._on(this.drop, "mouseover", inHandler); + this._on(this.target, "mouseout", outHandler); + this._on(this.drop, "mouseout", outHandler); + } + + if (events.indexOf("focus") >= 0) { + this._on(this.target, "focus", inHandler); + this._on(this.drop, "focus", inHandler); + this._on(this.target, "blur", outHandler); + this._on(this.drop, "blur", outHandler); + } + } + + isOpened() { + if (this.drop) { + return hasClass(this.drop, `${drop.classPrefix}-open`); + } + } + + toggle(event) { + if (this.isOpened()) { + this.close(event); + } else { + this.open(event); + } + } + + open(event) { + /* eslint no-unused-vars: 0 */ + if (this.isOpened()) { + return; + } + + if (!this.drop.parentNode) { + document.body.appendChild(this.drop); + } + + if (typeof this.tether !== "undefined") { + this.tether.enable(); + } + + addClass(this.drop, `${drop.classPrefix}-open`); + addClass(this.drop, `${drop.classPrefix}-open-transitionend`); + + setTimeout(() => { + if (this.drop) { + addClass(this.drop, `${drop.classPrefix}-after-open`); + } + }); + + this.trigger("beforeOpen"); + + if (typeof this.tether !== "undefined") { + this.tether.position(); + } + + this.trigger("open"); + + drop.updateBodyClasses(); + } + + _transitionEndHandler(e) { + if (e.target !== e.currentTarget) { + return; + } + + if (!hasClass(this.drop, `${drop.classPrefix}-open`)) { + removeClass(this.drop, `${drop.classPrefix}-open-transitionend`); + } + this.drop.removeEventListener( + transitionEndEvent, + this.transitionEndHandler + ); + } + + beforeCloseHandler(event) { + let shouldClose = true; + + if (!this.isClosing && typeof this.options.beforeClose === "function") { + this.isClosing = true; + shouldClose = this.options.beforeClose(event, this) !== false; + } + + this.isClosing = false; + + return shouldClose; + } + + close(event) { + if (!this.isOpened()) { + return; + } + + if (!this.beforeCloseHandler(event)) { + return; + } + + removeClass(this.drop, `${drop.classPrefix}-open`); + removeClass(this.drop, `${drop.classPrefix}-after-open`); + + this.drop.addEventListener(transitionEndEvent, this.transitionEndHandler); + + this.trigger("close"); + + if (typeof this.tether !== "undefined") { + this.tether.disable(); + } + + drop.updateBodyClasses(); + + if (this.options.remove) { + this.remove(event); + } + } + + remove(event) { + this.close(event); + if (this.drop.parentNode) { + this.drop.parentNode.removeChild(this.drop); + } + } + + position() { + if (this.isOpened() && typeof this.tether !== "undefined") { + this.tether.position(); + } + } + + destroy() { + this.remove(); + + if (typeof this.tether !== "undefined") { + this.tether.destroy(); + } + + for (let i = 0; i < this._boundEvents.length; ++i) { + const { element, event, handler } = this._boundEvents[i]; + element.removeEventListener(event, handler); + } + + this._boundEvents = []; + + this.tether = null; + this.drop = null; + this.content = null; + this.target = null; + + removeFromArray(allDrops[drop.classPrefix], this); + removeFromArray(drop.drops, this); + } + } + + return drop; +} + +const Drop = createContext(); + +export default Drop; + +document.addEventListener("DOMContentLoaded", () => { + Drop.updateBodyClasses(); +}); diff --git a/public/vendor/tether-drop/tetherUtils.js b/public/vendor/tether-drop/tetherUtils.js new file mode 100644 index 00000000000..405595334ee --- /dev/null +++ b/public/vendor/tether-drop/tetherUtils.js @@ -0,0 +1,378 @@ +let TetherBase; +if (typeof TetherBase === "undefined") { + TetherBase = { modules: [] }; +} + +let zeroElement = null; + +// Same as native getBoundingClientRect, except it takes into account parent offsets +// if the element lies within a nested document ( or