├── .github └── workflows │ ├── node.js.yml │ └── static.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── demo ├── index.css ├── index.html └── index.js ├── dist ├── drag-drop-touch.esm.js └── drag-drop-touch.esm.min.js ├── package-lock.json ├── package.json ├── server.js ├── tests ├── integration │ ├── index.css │ ├── index.html │ ├── index.js │ ├── issue-77b │ │ ├── index.html │ │ └── test.js │ └── touch-simulation.js └── touch.spec.js └── ts ├── drag-drop-touch-util.ts ├── drag-drop-touch.ts └── drag-dto.ts /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npx playwright install --with-deps 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v5 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload the repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/drag-drop-touch.debug.esm.js 3 | node_modules 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/* 2 | dist/drag-drop-touch.debug.esm.js 3 | node_modules/* 4 | tests/* 5 | ts/* 6 | .gitignore 7 | server.js 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2024 Bernardo Castilho 4 | Copyright (c) 2024 Pomax 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DragDropTouch 2 | 3 | Polyfill that enables HTML5 drag drop support on mobile (touch) devices. 4 | 5 | The HTML5 specification includes support for drag and drop operations. 6 | Unfortunately, this specification is based on mouse events, rather than 7 | pointer events, and so most mobile browsers do not implement it. As such, 8 | applications that rely on HTML5 drag and drop have reduced functionality 9 | when running on mobile devices. 10 | 11 | The `DragDropTouch` class is a polyfill that translates touch events into 12 | standard HTML5 drag drop events. If you add the polyfill to your pages, 13 | drag and drop operations should work on mobile devices just like they 14 | do on the desktop. 15 | 16 | ## Demo 17 | 18 | - [Click here to play with the demo](https://drag-drop-touch-js.github.io/dragdroptouch/demo/) 19 | 20 | This demo should work on desktop as well as on mobile devices, including 21 | iPads and Android tablets. To test this on a desktop, turn on "responsive 22 | design mode", which is both a button in the browser developer tools, as 23 | well as the hot-key ctrl-shift-M on Windows and Linux, or 24 | cmd-shift-M on Mac. 25 | 26 | ## How to "install" 27 | 28 | Add the `drag-drop-touch.esm.js` or `drag-drop-touch.esm.min.js` polyfill 29 | script to your page to enable drag and drop on devices with touch input: 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | Note the `?autoload` query argument on the `src` URL: this loads the polyfill 36 | and immediately enables it so that you do not need to write any code yourself. 37 | If omitted, the library will instead set up a `window.DragDropTouch` object 38 | with a single function, `DragDropTouch.enable(dragRoot, dropRoot, options)`. 39 | All three arguments are optional. If left off, `DragDropTouch.enable()` simply 40 | polyfills the entire page. If you only want the polyfill to apply to specific 41 | elements though, you can call the `enable` function once for each set of 42 | elements that need polyfilling. 43 | 44 | Also note the `type="module"`, which is required. If left off, you'll probably 45 | get a browser error similar to: 46 | 47 | ``` 48 | Uncaught SyntaxError: import.meta may only appear in a module 49 | ``` 50 | 51 | ## Using a CDN url 52 | 53 | ```html 54 | 58 | ``` 59 | 60 | ## Using a JS ESM import 61 | 62 | As an ES module, you can also use this polyfill as an import in other scripts: 63 | 64 | ```js 65 | import { enableDragDropTouch } from "./drag-drop-touch.esm.min.js"; 66 | 67 | // Set up the default full page polyfill: 68 | enableDragDropTouch(); 69 | 70 | // Or, explicitly polyfill only certain elements 71 | enableDragDropTouch(dragRootElement, dropRootElement); 72 | 73 | // Or even explicitly polyfill only certain elements with non-default behaviour 74 | const options = { 75 | // ... 76 | }; 77 | enableDragDropTouch(dragRootElement, dropRootElement, options); 78 | ``` 79 | 80 | ## Polyfill behaviour 81 | 82 | The **DragDropTouch** polyfill attaches listeners to the document's touch events: 83 | 84 | - On **touchstart**, it checks whether the target element has the draggable 85 | attribute or is contained in an element that does. If that is the case, it 86 | saves a reference to the "drag source" element and prevents the default 87 | handling of the event. 88 | - On **touchmove**, it checks whether the touch has moved a certain threshold 89 | distance from the origin. If that is the case, it raises the **dragstart** 90 | event and continues monitoring moves to fire **dragenter** and **dragleave**. 91 | - On **touchend**, it raises the **dragend** and **drop** events. 92 | 93 | To avoid interfering with the automatic browser translation of some touch events 94 | into mouse events, the polyfill performs a few additional tasks: 95 | 96 | - Raise the **mousemove**, **mousedown**, **mouseup**, and **click** events when 97 | the user touches a draggable element but doesn't start dragging, 98 | - Raise the **dblclick** event when there's a new touchstart right after a click, 99 | and 100 | - Raise the **contextmenu** event when the touch lasts a while but the user doesn't 101 | start dragging the element. 102 | 103 | ## Overriding polyfill behaviour 104 | 105 | The following options can be passed into the enabling function to change how the 106 | polyfill works: 107 | 108 | - **allowDragScroll** is a flag that determines whether to allow scrolling when 109 | a drag reaches the edges of the screen. This can be either `true` or `false`, 110 | and is `true` by default. 111 | - **contextMenuDelayMS** is the number of milliseconds we'll wait before the 112 | polyfill triggers a context menu event on long press. This value is 900 by 113 | default. 114 | - **dragImageOpacity** determines how see-through the "drag placeholder", that's 115 | attached to the cursor while dragging, should be. This value is a number in 116 | the interval [0, 1], where 0 means fully transparent, and 1 means fully opaque. 117 | This value is 0.5 by default. 118 | - **dragScrollPercentage** is the size of the "hot region" at the edge of the 119 | screen as a percentage value on which scrolling will be allowed, if the 120 | **allowDragScroll** flag is true (which is its default value). This value is 121 | 10 by default. 122 | - **dragScrollSpeed** is the number of pixels to scroll if a drag event occurs 123 | within a scrolling hot region. This value is 10 by default. 124 | - **dragThresholdPixels** is the number of pixels that a touchmove needs to 125 | actually move before the polyfill switches to drag mode rather than click mode. 126 | This value is 5 by default 127 | - **isPressHoldMode** is a flag that tells the polyfill whether a a long-press 128 | is required before polyfilling drag events. This value can be either `true` or 129 | `false`, and is `false` by default. 130 | - **forceListen** is a flag that determines whether the polyfill should be 131 | enabled irrespective of whether the browser indicates that it's running on 132 | a touch-enabled device or not. This value is `true` by default. 133 | - **pressHoldDelayMS**: is the number of milliseconds the polyfill will wait 134 | before it considers an active press to be a "long press". This value is 400 135 | by default. 136 | - **pressHoldMargin** is the number of pixels we allow a touch event to drift 137 | over the course of a long press start. This value is 25 by default. 138 | - **pressHoldThresholdPixels** is the drift in pixels that determines whether 139 | a long press actually starts a long press, or starts a touch-drag instead. 140 | This value is 0 by default. 141 | 142 | ## Thanks 143 | 144 | - Thanks to Eric Bidelman for the great tutorial on HTML5 drag and drop: 145 | [Native HTML5 Drag and Drop](http://www.html5rocks.com/en/tutorials/dnd/basics/). 146 | - Thanks also to Chris Wilson and Paul Kinlan for their article on mouse and touch events: 147 | [Touch And Mouse](http://www.html5rocks.com/en/mobile/touchandmouse/). 148 | - Thanks to Tim Ruffles for his iOS shim code which was inspiring: 149 | [iOS DragDrop Shim](https://github.com/timruffles/ios-html5-drag-drop-shim). 150 | 151 | ## License 152 | 153 | [MIT License](./LICENSE) 154 | 155 | ## For developers 156 | 157 | If you wish to work on this library, fork and clone the repository, then run 158 | `npm install` to install all the dependency, followed by a one-time 159 | `npm run dev:setup`, which will install the necessary components for running 160 | the integration tests. 161 | 162 | ### Running tests 163 | 164 | This repository uses the standard `npm test` command to run build and 165 | integration tests. Build testing consists of linting the source code using `tsc`, 166 | auto-formatting it using `prettier`, and compiling it into three bundles (debug, normal, and minified) using `esbuild`. Integration tests are found in the 167 | `tests/touch.spec.js` file, using Playwright as test runner. 168 | 169 | Additionally, `npm run test:debug` will run the tests with `DEBUG` statements preserved, useful for when tests fail to pass and you're trying to find out what's actually happening. 170 | 171 | ### Manual testing 172 | 173 | To manually test in the browser, you can run `npm start` and then open the 174 | URL that is printed to the console once the initial build tasks have finished. 175 | This runs a local server that lets you run the demo page, but with the 176 | `drag-drop-touch.esm.min.js` replaced by a `drag-drop-touch.debug.esm.js` 177 | instead, which preserves all debug statements used in the TypeScript source. 178 | 179 | To add your own debug statements, use the `DEBUG:` label followed by either 180 | a normal statement, or multiple statements wrapped in a new block. 181 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | [draggable] { 2 | user-select: none; 3 | } 4 | 5 | .dragging { 6 | opacity: 0.5; 7 | } 8 | 9 | #columns { 10 | display: flex; 11 | gap: 1rem; 12 | 13 | .column { 14 | display: inline-block; 15 | height: 150px; 16 | width: 150px; 17 | background: lightgrey; 18 | opacity: 1; 19 | 20 | header { 21 | color: #fff; 22 | padding: 5px; 23 | background: #222; 24 | pointer-events: none; 25 | } 26 | 27 | &.over { 28 | border: 2px dashed #000; 29 | opacity: 0.5; 30 | box-sizing: border-box; 31 | } 32 | 33 | & > input, 34 | & > textarea, 35 | & > select { 36 | display: block; 37 | width: 85%; 38 | margin: 5%; 39 | max-height: calc(1.25em * 4); 40 | } 41 | 42 | & > img { 43 | height: 50%; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | DragDropTouch 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

Drag and Drop Touch

15 |

16 | This is a demo page that loads the drag-drop-touch.esm.js file for 17 | shimming drag-and-drop functions for touch events. 18 |

19 |

20 | Its project page is over on 21 | https://github.com/drag-drop-touch-js/dragdroptouch. 24 |

25 |
26 | 27 |
28 |
29 |
30 |

A box dragging example

31 |

32 | Drag some boxes around with the mouse, then open your Developer 33 | Tools, turn on mobile emulation, and try to do the same with touch 34 | input enabled. Things should still work. 35 |

36 |
37 | 38 |
39 |
40 |
Input
41 | 47 | 48 |
49 | 50 |
51 |
TextArea
52 | 53 |
54 | 55 |
56 |
Select
57 | 62 | 67 |
68 | 69 |
70 |
Image
71 | grapefruit 76 |
77 |
78 |
79 |
80 | 81 | 82 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | let draggable = null; 2 | const cols = document.querySelectorAll(`#columns .column`); 3 | 4 | cols.forEach((col) => { 5 | col.addEventListener(`dragstart`, handleDragStart); 6 | col.addEventListener(`dragenter`, handleDragEnter); 7 | col.addEventListener(`dragover`, handleDragOver); 8 | col.addEventListener(`dragleave`, handleDragLeave); 9 | col.addEventListener(`drop`, handleDrop); 10 | col.addEventListener(`dragend`, handleDragEnd); 11 | }); 12 | 13 | function handleDragStart({ target, dataTransfer }) { 14 | if (target.className.includes(`column`)) { 15 | draggable = target; 16 | draggable.classList.add(`dragging`); 17 | 18 | dataTransfer.effectAllowed = `move`; 19 | dataTransfer.setData(`text`, draggable.innerHTML); 20 | 21 | // customize drag image for one of the panels 22 | const haveDragFn = dataTransfer.setDragImage instanceof Function; 23 | if (haveDragFn && target.textContent.includes(`X`)) { 24 | let img = new Image(); 25 | img.src = `dragimage.jpg`; 26 | dataTransfer.setDragImage(img, img.width / 2, img.height / 2); 27 | } 28 | } 29 | } 30 | 31 | function handleDragOver(evt) { 32 | if (draggable) { 33 | evt.preventDefault(); 34 | evt.dataTransfer.dropEffect = `move`; 35 | } 36 | } 37 | 38 | function handleDragEnter({ target }) { 39 | if (draggable) { 40 | target.classList.add(`over`); 41 | } 42 | } 43 | 44 | function handleDragLeave({ target }) { 45 | if (draggable) { 46 | target.classList.remove(`over`); 47 | } 48 | } 49 | 50 | function handleDragEnd() { 51 | draggable = null; 52 | cols.forEach((col) => col.classList.remove(`over`)); 53 | } 54 | 55 | function handleDrop(evt) { 56 | if (draggable === null) return; 57 | 58 | evt.stopPropagation(); 59 | evt.stopImmediatePropagation(); 60 | evt.preventDefault(); 61 | 62 | if (draggable !== this) { 63 | swapDom(draggable, this); 64 | } 65 | } 66 | 67 | // https://stackoverflow.com/questions/9732624/how-to-swap-dom-child-nodes-in-javascript 68 | function swapDom(a, b) { 69 | let aParent = a.parentNode; 70 | let bParent = b.parentNode; 71 | let aHolder = document.createElement(`div`); 72 | let bHolder = document.createElement(`div`); 73 | aParent.replaceChild(aHolder, a); 74 | bParent.replaceChild(bHolder, b); 75 | aParent.replaceChild(b, aHolder); 76 | bParent.replaceChild(a, bHolder); 77 | } 78 | -------------------------------------------------------------------------------- /dist/drag-drop-touch.esm.js: -------------------------------------------------------------------------------- 1 | // ts/drag-drop-touch-util.ts 2 | function pointFrom(e, page = false) { 3 | const touch = e.touches[0]; 4 | return { 5 | x: page ? touch.pageX : touch.clientX, 6 | y: page ? touch.pageY : touch.clientY 7 | }; 8 | } 9 | function copyProps(dst, src, props) { 10 | for (let i = 0; i < props.length; i++) { 11 | let p = props[i]; 12 | dst[p] = src[p]; 13 | } 14 | } 15 | function newForwardableEvent(type, srcEvent, target) { 16 | const _kbdProps = ["altKey", "ctrlKey", "metaKey", "shiftKey"]; 17 | const _ptProps = [ 18 | "pageX", 19 | "pageY", 20 | "clientX", 21 | "clientY", 22 | "screenX", 23 | "screenY", 24 | "offsetX", 25 | "offsetY" 26 | ]; 27 | const evt = new Event(type, { 28 | bubbles: true, 29 | cancelable: true 30 | }), touch = srcEvent.touches[0]; 31 | evt.button = 0; 32 | evt.which = evt.buttons = 1; 33 | copyProps(evt, srcEvent, _kbdProps); 34 | copyProps(evt, touch, _ptProps); 35 | setOffsetAndLayerProps(evt, target); 36 | return evt; 37 | } 38 | function setOffsetAndLayerProps(e, target) { 39 | const rect = target.getBoundingClientRect(); 40 | if (e.offsetX === void 0) { 41 | e.offsetX = e.clientX - rect.x; 42 | e.offsetY = e.clientY - rect.y; 43 | } 44 | if (e.layerX === void 0) { 45 | e.layerX = e.pageX - rect.left; 46 | e.layerY = e.pageY - rect.top; 47 | } 48 | } 49 | function copyStyle(src, dst) { 50 | removeTroublesomeAttributes(dst); 51 | if (src instanceof HTMLCanvasElement) { 52 | let cDst = dst; 53 | cDst.width = src.width; 54 | cDst.height = src.height; 55 | cDst.getContext("2d").drawImage(src, 0, 0); 56 | } 57 | copyComputedStyles(src, dst); 58 | dst.style.pointerEvents = "none"; 59 | for (let i = 0; i < src.children.length; i++) { 60 | copyStyle(src.children[i], dst.children[i]); 61 | } 62 | } 63 | function copyComputedStyles(src, dst) { 64 | let cs = getComputedStyle(src); 65 | for (let key of cs) { 66 | if (key.includes("transition")) continue; 67 | dst.style[key] = cs[key]; 68 | } 69 | Object.keys(dst.dataset).forEach((key) => delete dst.dataset[key]); 70 | } 71 | function removeTroublesomeAttributes(dst) { 72 | ["id", "class", "style", "draggable"].forEach(function(att) { 73 | dst.removeAttribute(att); 74 | }); 75 | } 76 | 77 | // ts/drag-dto.ts 78 | var DragDTO = class { 79 | _dropEffect; 80 | _effectAllowed; 81 | _data; 82 | _dragDropTouch; 83 | constructor(dragDropTouch) { 84 | this._dropEffect = "move"; 85 | this._effectAllowed = "all"; 86 | this._data = {}; 87 | this._dragDropTouch = dragDropTouch; 88 | } 89 | get dropEffect() { 90 | return this._dropEffect; 91 | } 92 | set dropEffect(value) { 93 | this._dropEffect = value; 94 | } 95 | get effectAllowed() { 96 | return this._effectAllowed; 97 | } 98 | set effectAllowed(value) { 99 | this._effectAllowed = value; 100 | } 101 | get types() { 102 | return Object.keys(this._data); 103 | } 104 | /** 105 | * ...docs go here... 106 | * @param type 107 | */ 108 | clearData(type) { 109 | if (type !== null) { 110 | delete this._data[type.toLowerCase()]; 111 | } else { 112 | this._data = {}; 113 | } 114 | } 115 | /** 116 | * ...docs go here... 117 | * @param type 118 | * @returns 119 | */ 120 | getData(type) { 121 | let lcType = type.toLowerCase(), data = this._data[lcType]; 122 | if (lcType === "text" && data == null) { 123 | data = this._data["text/plain"]; 124 | } 125 | return data; 126 | } 127 | /** 128 | * ...docs go here... 129 | * @param type 130 | * @param value 131 | */ 132 | setData(type, value) { 133 | this._data[type.toLowerCase()] = value; 134 | } 135 | /** 136 | * ...docs go here... 137 | * @param img 138 | * @param offsetX 139 | * @param offsetY 140 | */ 141 | setDragImage(img, offsetX, offsetY) { 142 | this._dragDropTouch.setDragImage(img, offsetX, offsetY); 143 | } 144 | }; 145 | 146 | // ts/drag-drop-touch.ts 147 | var { round } = Math; 148 | var DefaultConfiguration = { 149 | allowDragScroll: true, 150 | contextMenuDelayMS: 900, 151 | dragImageOpacity: 0.5, 152 | dragScrollPercentage: 10, 153 | dragScrollSpeed: 10, 154 | dragThresholdPixels: 5, 155 | forceListen: false, 156 | isPressHoldMode: false, 157 | pressHoldDelayMS: 400, 158 | pressHoldMargin: 25, 159 | pressHoldThresholdPixels: 0 160 | }; 161 | var DragDropTouch = class { 162 | _dragRoot; 163 | _dropRoot; 164 | _dragSource; 165 | _lastTouch; 166 | _lastTarget; 167 | _ptDown; 168 | _isDragEnabled; 169 | _isDropZone; 170 | _dataTransfer; 171 | _img; 172 | _imgCustom; 173 | _imgOffset; 174 | _pressHoldIntervalId; 175 | configuration; 176 | /** 177 | * Deal with shadow DOM elements. 178 | * 179 | * Previous implementation used `document.elementFromPoint` to find the dropped upon 180 | * element. This, however, doesn't "pierce" the shadow DOM. So instead, we can 181 | * provide a drop tree element to search within. It would be nice if `elementFromPoint` 182 | * were implemented on this node (arbitrarily), but it only appears on documents and 183 | * shadow roots. So here we simply walk up the DOM tree until we find that method. 184 | * 185 | * In fact this does NOT restrict dropping to just the root provided-- but the whole 186 | * tree. I'm not sure that this is a general solution, but works for my specific and 187 | * the general one. 188 | * 189 | * @param dragRoot 190 | * @param options 191 | */ 192 | constructor(dragRoot = document, dropRoot = document, options) { 193 | this.configuration = { ...DefaultConfiguration, ...options || {} }; 194 | this._dragRoot = dragRoot; 195 | this._dropRoot = dropRoot; 196 | while (!this._dropRoot.elementFromPoint && this._dropRoot.parentNode) 197 | this._dropRoot = this._dropRoot.parentNode; 198 | this._dragSource = null; 199 | this._lastTouch = null; 200 | this._lastTarget = null; 201 | this._ptDown = null; 202 | this._isDragEnabled = false; 203 | this._isDropZone = false; 204 | this._dataTransfer = new DragDTO(this); 205 | this._img = null; 206 | this._imgCustom = null; 207 | this._imgOffset = { x: 0, y: 0 }; 208 | this.listen(); 209 | } 210 | /** 211 | * ...docs go here... 212 | * @returns 213 | */ 214 | listen() { 215 | if (navigator.maxTouchPoints === 0 && !this.configuration.forceListen) { 216 | return; 217 | } 218 | const opt = { passive: false, capture: false }; 219 | this._dragRoot.addEventListener( 220 | `touchstart`, 221 | this._touchstart.bind(this), 222 | opt 223 | ); 224 | this._dragRoot.addEventListener( 225 | `touchmove`, 226 | this._touchmove.bind(this), 227 | opt 228 | ); 229 | this._dragRoot.addEventListener( 230 | `touchend`, 231 | this._touchend.bind(this) 232 | ); 233 | this._dragRoot.addEventListener( 234 | `touchcancel`, 235 | this._touchend.bind(this) 236 | ); 237 | } 238 | /** 239 | * ...docs go here... 240 | * @param img 241 | * @param offsetX 242 | * @param offsetY 243 | */ 244 | setDragImage(img, offsetX, offsetY) { 245 | this._imgCustom = img; 246 | this._imgOffset = { x: offsetX, y: offsetY }; 247 | } 248 | /** 249 | * ...docs go here... 250 | * @param e 251 | */ 252 | _touchstart(e) { 253 | if (this._shouldHandle(e)) { 254 | this._reset(); 255 | let src = this._closestDraggable(e.target); 256 | if (src) { 257 | if (e.target && !this._dispatchEvent(e, `mousemove`, e.target) && !this._dispatchEvent(e, `mousedown`, e.target)) { 258 | this._dragSource = src; 259 | this._ptDown = pointFrom(e); 260 | this._lastTouch = e; 261 | setTimeout(() => { 262 | if (this._dragSource === src && this._img === null) { 263 | if (this._dispatchEvent(e, `contextmenu`, src)) { 264 | this._reset(); 265 | } 266 | } 267 | }, this.configuration.contextMenuDelayMS); 268 | if (this.configuration.isPressHoldMode) { 269 | this._pressHoldIntervalId = setTimeout(() => { 270 | this._isDragEnabled = true; 271 | this._touchmove(e); 272 | }, this.configuration.pressHoldDelayMS); 273 | } else if (!e.isTrusted) { 274 | if (e.target !== this._lastTarget) { 275 | this._lastTarget = e.target; 276 | } 277 | } 278 | } 279 | } 280 | } 281 | } 282 | /** 283 | * ...docs go here... 284 | * @param e 285 | * @returns 286 | */ 287 | _touchmove(e) { 288 | if (this._shouldCancelPressHoldMove(e)) { 289 | this._reset(); 290 | return; 291 | } 292 | if (this._shouldHandleMove(e) || this._shouldHandlePressHoldMove(e)) { 293 | let target = this._getTarget(e); 294 | if (this._dispatchEvent(e, `mousemove`, target)) { 295 | this._lastTouch = e; 296 | e.preventDefault(); 297 | return; 298 | } 299 | if (this._dragSource && !this._img && this._shouldStartDragging(e)) { 300 | if (this._dispatchEvent(this._lastTouch, `dragstart`, this._dragSource)) { 301 | this._dragSource = null; 302 | return; 303 | } 304 | this._createImage(e); 305 | this._dispatchEvent(e, `dragenter`, target); 306 | } 307 | if (this._img && this._dragSource) { 308 | this._lastTouch = e; 309 | e.preventDefault(); 310 | this._dispatchEvent(e, `drag`, this._dragSource); 311 | if (target !== this._lastTarget) { 312 | if (this._lastTarget) 313 | this._dispatchEvent(this._lastTouch, `dragleave`, this._lastTarget); 314 | this._dispatchEvent(e, `dragenter`, target); 315 | this._lastTarget = target; 316 | } 317 | this._moveImage(e); 318 | this._isDropZone = this._dispatchEvent(e, `dragover`, target); 319 | if (this.configuration.allowDragScroll) { 320 | const delta = this._getHotRegionDelta(e); 321 | globalThis.scrollBy(delta.x, delta.y); 322 | } 323 | } 324 | } 325 | } 326 | /** 327 | * ...docs go here... 328 | * @param e 329 | * @returns 330 | */ 331 | _touchend(e) { 332 | if (!(this._lastTouch && e.target && this._lastTarget)) { 333 | this._reset(); 334 | return; 335 | } 336 | if (this._shouldHandle(e)) { 337 | if (this._dispatchEvent(this._lastTouch, `mouseup`, e.target)) { 338 | e.preventDefault(); 339 | return; 340 | } 341 | if (!this._img) { 342 | this._dragSource = null; 343 | this._dispatchEvent(this._lastTouch, `click`, e.target); 344 | } 345 | this._destroyImage(); 346 | if (this._dragSource) { 347 | if (e.type.indexOf(`cancel`) < 0 && this._isDropZone) { 348 | this._dispatchEvent(this._lastTouch, `drop`, this._lastTarget); 349 | } 350 | this._dispatchEvent(this._lastTouch, `dragend`, this._dragSource); 351 | this._reset(); 352 | } 353 | } 354 | } 355 | /** 356 | * ...docs go here... 357 | * @param e 358 | * @returns 359 | */ 360 | _shouldHandle(e) { 361 | return e && !e.defaultPrevented && e.touches && e.touches.length < 2; 362 | } 363 | /** 364 | * ...docs go here... 365 | * @param e 366 | * @returns 367 | */ 368 | _shouldHandleMove(e) { 369 | return !this.configuration.isPressHoldMode && this._shouldHandle(e); 370 | } 371 | /** 372 | * ...docs go here... 373 | * @param e 374 | * @returns 375 | */ 376 | _shouldHandlePressHoldMove(e) { 377 | return this.configuration.isPressHoldMode && this._isDragEnabled && e && e.touches && e.touches.length; 378 | } 379 | /** 380 | * ...docs go here... 381 | * @param e 382 | * @returns 383 | */ 384 | _shouldCancelPressHoldMove(e) { 385 | return this.configuration.isPressHoldMode && !this._isDragEnabled && this._getDelta(e) > this.configuration.pressHoldMargin; 386 | } 387 | /** 388 | * ...docs go here... 389 | * @param e 390 | * @returns 391 | */ 392 | _shouldStartDragging(e) { 393 | let delta = this._getDelta(e); 394 | if (this.configuration.isPressHoldMode) { 395 | return delta >= this.configuration.pressHoldThresholdPixels; 396 | } 397 | return delta > this.configuration.dragThresholdPixels; 398 | } 399 | /** 400 | * ...docs go here... 401 | */ 402 | _reset() { 403 | this._destroyImage(); 404 | this._dragSource = null; 405 | this._lastTouch = null; 406 | this._lastTarget = null; 407 | this._ptDown = null; 408 | this._isDragEnabled = false; 409 | this._isDropZone = false; 410 | this._dataTransfer = new DragDTO(this); 411 | clearTimeout(this._pressHoldIntervalId); 412 | } 413 | /** 414 | * ...docs go here... 415 | * @param e 416 | * @returns 417 | */ 418 | _getDelta(e) { 419 | if (!this._ptDown) return 0; 420 | const { x, y } = this._ptDown; 421 | const p = pointFrom(e); 422 | return ((p.x - x) ** 2 + (p.y - y) ** 2) ** 0.5; 423 | } 424 | /** 425 | * ...docs go here... 426 | * @param e 427 | */ 428 | _getHotRegionDelta(e) { 429 | const { clientX: x, clientY: y } = e.touches[0]; 430 | const { innerWidth: w, innerHeight: h } = globalThis; 431 | const { dragScrollPercentage, dragScrollSpeed } = this.configuration; 432 | const v1 = dragScrollPercentage / 100; 433 | const v2 = 1 - v1; 434 | const dx = x < w * v1 ? -dragScrollSpeed : x > w * v2 ? +dragScrollSpeed : 0; 435 | const dy = y < h * v1 ? -dragScrollSpeed : y > h * v2 ? +dragScrollSpeed : 0; 436 | return { x: dx, y: dy }; 437 | } 438 | /** 439 | * ...docs go here... 440 | * @param e 441 | * @returns 442 | */ 443 | _getTarget(e) { 444 | let pt = pointFrom(e), el = this._dropRoot.elementFromPoint(pt.x, pt.y); 445 | while (el && getComputedStyle(el).pointerEvents == `none`) { 446 | el = el.parentElement; 447 | } 448 | return el; 449 | } 450 | /** 451 | * ...docs go here... 452 | * @param e 453 | */ 454 | _createImage(e) { 455 | if (this._img) { 456 | this._destroyImage(); 457 | } 458 | let src = this._imgCustom || this._dragSource; 459 | this._img = src.cloneNode(true); 460 | copyStyle(src, this._img); 461 | this._img.style.top = this._img.style.left = `-9999px`; 462 | if (!this._imgCustom) { 463 | let rc = src.getBoundingClientRect(), pt = pointFrom(e); 464 | this._imgOffset = { x: pt.x - rc.left, y: pt.y - rc.top }; 465 | this._img.style.opacity = `${this.configuration.dragImageOpacity}`; 466 | } 467 | this._moveImage(e); 468 | document.body.appendChild(this._img); 469 | } 470 | /** 471 | * ...docs go here... 472 | */ 473 | _destroyImage() { 474 | if (this._img && this._img.parentElement) { 475 | this._img.parentElement.removeChild(this._img); 476 | } 477 | this._img = null; 478 | this._imgCustom = null; 479 | } 480 | /** 481 | * ...docs go here... 482 | * @param e 483 | */ 484 | _moveImage(e) { 485 | requestAnimationFrame(() => { 486 | if (this._img) { 487 | let pt = pointFrom(e, true), s = this._img.style; 488 | s.position = `absolute`; 489 | s.pointerEvents = `none`; 490 | s.zIndex = `999999`; 491 | s.left = `${round(pt.x - this._imgOffset.x)}px`; 492 | s.top = `${round(pt.y - this._imgOffset.y)}px`; 493 | } 494 | }); 495 | } 496 | /** 497 | * ...docs go here... 498 | * @param srcEvent 499 | * @param type 500 | * @param target 501 | * @returns 502 | */ 503 | _dispatchEvent(srcEvent, type, target) { 504 | if (!(srcEvent && target)) return false; 505 | const evt = newForwardableEvent(type, srcEvent, target); 506 | evt.dataTransfer = this._dataTransfer; 507 | target.dispatchEvent(evt); 508 | return evt.defaultPrevented; 509 | } 510 | /** 511 | * ...docs go here... 512 | * @param el 513 | * @returns 514 | */ 515 | _closestDraggable(element) { 516 | for (let e = element; e !== null; e = e.parentElement) { 517 | if (e.draggable) { 518 | return e; 519 | } 520 | } 521 | return null; 522 | } 523 | }; 524 | function enableDragDropTouch(dragRoot = document, dropRoot = document, options) { 525 | new DragDropTouch(dragRoot, dropRoot, options); 526 | } 527 | if (import.meta.url.includes(`?autoload`)) { 528 | enableDragDropTouch(document, document, { 529 | forceListen: true 530 | }); 531 | } else { 532 | globalThis.DragDropTouch = { 533 | enable: function(dragRoot = document, dropRoot = document, options) { 534 | enableDragDropTouch(dragRoot, dropRoot, options); 535 | } 536 | }; 537 | } 538 | export { 539 | enableDragDropTouch 540 | }; 541 | -------------------------------------------------------------------------------- /dist/drag-drop-touch.esm.min.js: -------------------------------------------------------------------------------- 1 | function a(s,t=!1){let e=s.touches[0];return{x:t?e.pageX:e.clientX,y:t?e.pageY:e.clientY}}function _(s,t,e){for(let o=0;odelete t.dataset[o])}function D(s){["id","class","style","draggable"].forEach(function(t){s.removeAttribute(t)})}var r=class{_dropEffect;_effectAllowed;_data;_dragDropTouch;constructor(t){this._dropEffect="move",this._effectAllowed="all",this._data={},this._dragDropTouch=t}get dropEffect(){return this._dropEffect}set dropEffect(t){this._dropEffect=t}get effectAllowed(){return this._effectAllowed}set effectAllowed(t){this._effectAllowed=t}get types(){return Object.keys(this._data)}clearData(t){t!==null?delete this._data[t.toLowerCase()]:this._data={}}getData(t){let e=t.toLowerCase(),o=this._data[e];return e==="text"&&o==null&&(o=this._data["text/plain"]),o}setData(t,e){this._data[t.toLowerCase()]=e}setDragImage(t,e,o){this._dragDropTouch.setDragImage(t,e,o)}};var{round:m}=Math,b={allowDragScroll:!0,contextMenuDelayMS:900,dragImageOpacity:.5,dragScrollPercentage:10,dragScrollSpeed:10,dragThresholdPixels:5,forceListen:!1,isPressHoldMode:!1,pressHoldDelayMS:400,pressHoldMargin:25,pressHoldThresholdPixels:0},d=class{_dragRoot;_dropRoot;_dragSource;_lastTouch;_lastTarget;_ptDown;_isDragEnabled;_isDropZone;_dataTransfer;_img;_imgCustom;_imgOffset;_pressHoldIntervalId;configuration;constructor(t=document,e=document,o){for(this.configuration={...b,...o||{}},this._dragRoot=t,this._dropRoot=e;!this._dropRoot.elementFromPoint&&this._dropRoot.parentNode;)this._dropRoot=this._dropRoot.parentNode;this._dragSource=null,this._lastTouch=null,this._lastTarget=null,this._ptDown=null,this._isDragEnabled=!1,this._isDropZone=!1,this._dataTransfer=new r(this),this._img=null,this._imgCustom=null,this._imgOffset={x:0,y:0},this.listen()}listen(){if(navigator.maxTouchPoints===0&&!this.configuration.forceListen)return;let t={passive:!1,capture:!1};this._dragRoot.addEventListener("touchstart",this._touchstart.bind(this),t),this._dragRoot.addEventListener("touchmove",this._touchmove.bind(this),t),this._dragRoot.addEventListener("touchend",this._touchend.bind(this)),this._dragRoot.addEventListener("touchcancel",this._touchend.bind(this))}setDragImage(t,e,o){this._imgCustom=t,this._imgOffset={x:e,y:o}}_touchstart(t){if(this._shouldHandle(t)){this._reset();let e=this._closestDraggable(t.target);e&&t.target&&!this._dispatchEvent(t,"mousemove",t.target)&&!this._dispatchEvent(t,"mousedown",t.target)&&(this._dragSource=e,this._ptDown=a(t),this._lastTouch=t,setTimeout(()=>{this._dragSource===e&&this._img===null&&this._dispatchEvent(t,"contextmenu",e)&&this._reset()},this.configuration.contextMenuDelayMS),this.configuration.isPressHoldMode?this._pressHoldIntervalId=setTimeout(()=>{this._isDragEnabled=!0,this._touchmove(t)},this.configuration.pressHoldDelayMS):t.isTrusted||t.target!==this._lastTarget&&(this._lastTarget=t.target))}}_touchmove(t){if(this._shouldCancelPressHoldMove(t)){this._reset();return}if(this._shouldHandleMove(t)||this._shouldHandlePressHoldMove(t)){let e=this._getTarget(t);if(this._dispatchEvent(t,"mousemove",e)){this._lastTouch=t,t.preventDefault();return}if(this._dragSource&&!this._img&&this._shouldStartDragging(t)){if(this._dispatchEvent(this._lastTouch,"dragstart",this._dragSource)){this._dragSource=null;return}this._createImage(t),this._dispatchEvent(t,"dragenter",e)}if(this._img&&this._dragSource&&(this._lastTouch=t,t.preventDefault(),this._dispatchEvent(t,"drag",this._dragSource),e!==this._lastTarget&&(this._lastTarget&&this._dispatchEvent(this._lastTouch,"dragleave",this._lastTarget),this._dispatchEvent(t,"dragenter",e),this._lastTarget=e),this._moveImage(t),this._isDropZone=this._dispatchEvent(t,"dragover",e),this.configuration.allowDragScroll)){let o=this._getHotRegionDelta(t);globalThis.scrollBy(o.x,o.y)}}}_touchend(t){if(!(this._lastTouch&&t.target&&this._lastTarget)){this._reset();return}if(this._shouldHandle(t)){if(this._dispatchEvent(this._lastTouch,"mouseup",t.target)){t.preventDefault();return}this._img||(this._dragSource=null,this._dispatchEvent(this._lastTouch,"click",t.target)),this._destroyImage(),this._dragSource&&(t.type.indexOf("cancel")<0&&this._isDropZone&&this._dispatchEvent(this._lastTouch,"drop",this._lastTarget),this._dispatchEvent(this._lastTouch,"dragend",this._dragSource),this._reset())}}_shouldHandle(t){return t&&!t.defaultPrevented&&t.touches&&t.touches.length<2}_shouldHandleMove(t){return!this.configuration.isPressHoldMode&&this._shouldHandle(t)}_shouldHandlePressHoldMove(t){return this.configuration.isPressHoldMode&&this._isDragEnabled&&t&&t.touches&&t.touches.length}_shouldCancelPressHoldMove(t){return this.configuration.isPressHoldMode&&!this._isDragEnabled&&this._getDelta(t)>this.configuration.pressHoldMargin}_shouldStartDragging(t){let e=this._getDelta(t);return this.configuration.isPressHoldMode?e>=this.configuration.pressHoldThresholdPixels:e>this.configuration.dragThresholdPixels}_reset(){this._destroyImage(),this._dragSource=null,this._lastTouch=null,this._lastTarget=null,this._ptDown=null,this._isDragEnabled=!1,this._isDropZone=!1,this._dataTransfer=new r(this),clearTimeout(this._pressHoldIntervalId)}_getDelta(t){if(!this._ptDown)return 0;let{x:e,y:o}=this._ptDown,n=a(t);return((n.x-e)**2+(n.y-o)**2)**.5}_getHotRegionDelta(t){let{clientX:e,clientY:o}=t.touches[0],{innerWidth:n,innerHeight:i}=globalThis,{dragScrollPercentage:h,dragScrollSpeed:l}=this.configuration,u=h/100,g=1-u,v=en*g?+l:0,T=oi*g?+l:0;return{x:v,y:T}}_getTarget(t){let e=a(t),o=this._dropRoot.elementFromPoint(e.x,e.y);for(;o&&getComputedStyle(o).pointerEvents=="none";)o=o.parentElement;return o}_createImage(t){this._img&&this._destroyImage();let e=this._imgCustom||this._dragSource;if(this._img=e.cloneNode(!0),c(e,this._img),this._img.style.top=this._img.style.left="-9999px",!this._imgCustom){let o=e.getBoundingClientRect(),n=a(t);this._imgOffset={x:n.x-o.left,y:n.y-o.top},this._img.style.opacity=`${this.configuration.dragImageOpacity}`}this._moveImage(t),document.body.appendChild(this._img)}_destroyImage(){this._img&&this._img.parentElement&&this._img.parentElement.removeChild(this._img),this._img=null,this._imgCustom=null}_moveImage(t){requestAnimationFrame(()=>{if(this._img){let e=a(t,!0),o=this._img.style;o.position="absolute",o.pointerEvents="none",o.zIndex="999999",o.left=`${m(e.x-this._imgOffset.x)}px`,o.top=`${m(e.y-this._imgOffset.y)}px`}})}_dispatchEvent(t,e,o){if(!(t&&o))return!1;let n=f(e,t,o);return n.dataTransfer=this._dataTransfer,o.dispatchEvent(n),n.defaultPrevented}_closestDraggable(t){for(let e=t;e!==null;e=e.parentElement)if(e.draggable)return e;return null}};function p(s=document,t=document,e){new d(s,t,e)}import.meta.url.includes("?autoload")?p(document,document,{forceListen:!0}):globalThis.DragDropTouch={enable:function(s=document,t=document,e){p(s,t,e)}};export{p as enableDragDropTouch}; 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drag-drop-touch", 3 | "version": "2.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "drag-drop-touch", 9 | "version": "2.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@playwright/test": "^1.45.2", 13 | "esbuild": "^0.23.0", 14 | "express": "^4.19.2", 15 | "playwright": "^1.45.2", 16 | "prettier": "^3.3.3", 17 | "typescript": "^5.5.4" 18 | } 19 | }, 20 | "node_modules/@esbuild/aix-ppc64": { 21 | "version": "0.23.0", 22 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", 23 | "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", 24 | "cpu": [ 25 | "ppc64" 26 | ], 27 | "dev": true, 28 | "optional": true, 29 | "os": [ 30 | "aix" 31 | ], 32 | "engines": { 33 | "node": ">=18" 34 | } 35 | }, 36 | "node_modules/@esbuild/android-arm": { 37 | "version": "0.23.0", 38 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", 39 | "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", 40 | "cpu": [ 41 | "arm" 42 | ], 43 | "dev": true, 44 | "optional": true, 45 | "os": [ 46 | "android" 47 | ], 48 | "engines": { 49 | "node": ">=18" 50 | } 51 | }, 52 | "node_modules/@esbuild/android-arm64": { 53 | "version": "0.23.0", 54 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", 55 | "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", 56 | "cpu": [ 57 | "arm64" 58 | ], 59 | "dev": true, 60 | "optional": true, 61 | "os": [ 62 | "android" 63 | ], 64 | "engines": { 65 | "node": ">=18" 66 | } 67 | }, 68 | "node_modules/@esbuild/android-x64": { 69 | "version": "0.23.0", 70 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", 71 | "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", 72 | "cpu": [ 73 | "x64" 74 | ], 75 | "dev": true, 76 | "optional": true, 77 | "os": [ 78 | "android" 79 | ], 80 | "engines": { 81 | "node": ">=18" 82 | } 83 | }, 84 | "node_modules/@esbuild/darwin-arm64": { 85 | "version": "0.23.0", 86 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", 87 | "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", 88 | "cpu": [ 89 | "arm64" 90 | ], 91 | "dev": true, 92 | "optional": true, 93 | "os": [ 94 | "darwin" 95 | ], 96 | "engines": { 97 | "node": ">=18" 98 | } 99 | }, 100 | "node_modules/@esbuild/darwin-x64": { 101 | "version": "0.23.0", 102 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", 103 | "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", 104 | "cpu": [ 105 | "x64" 106 | ], 107 | "dev": true, 108 | "optional": true, 109 | "os": [ 110 | "darwin" 111 | ], 112 | "engines": { 113 | "node": ">=18" 114 | } 115 | }, 116 | "node_modules/@esbuild/freebsd-arm64": { 117 | "version": "0.23.0", 118 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", 119 | "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", 120 | "cpu": [ 121 | "arm64" 122 | ], 123 | "dev": true, 124 | "optional": true, 125 | "os": [ 126 | "freebsd" 127 | ], 128 | "engines": { 129 | "node": ">=18" 130 | } 131 | }, 132 | "node_modules/@esbuild/freebsd-x64": { 133 | "version": "0.23.0", 134 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", 135 | "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", 136 | "cpu": [ 137 | "x64" 138 | ], 139 | "dev": true, 140 | "optional": true, 141 | "os": [ 142 | "freebsd" 143 | ], 144 | "engines": { 145 | "node": ">=18" 146 | } 147 | }, 148 | "node_modules/@esbuild/linux-arm": { 149 | "version": "0.23.0", 150 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", 151 | "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", 152 | "cpu": [ 153 | "arm" 154 | ], 155 | "dev": true, 156 | "optional": true, 157 | "os": [ 158 | "linux" 159 | ], 160 | "engines": { 161 | "node": ">=18" 162 | } 163 | }, 164 | "node_modules/@esbuild/linux-arm64": { 165 | "version": "0.23.0", 166 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", 167 | "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", 168 | "cpu": [ 169 | "arm64" 170 | ], 171 | "dev": true, 172 | "optional": true, 173 | "os": [ 174 | "linux" 175 | ], 176 | "engines": { 177 | "node": ">=18" 178 | } 179 | }, 180 | "node_modules/@esbuild/linux-ia32": { 181 | "version": "0.23.0", 182 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", 183 | "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", 184 | "cpu": [ 185 | "ia32" 186 | ], 187 | "dev": true, 188 | "optional": true, 189 | "os": [ 190 | "linux" 191 | ], 192 | "engines": { 193 | "node": ">=18" 194 | } 195 | }, 196 | "node_modules/@esbuild/linux-loong64": { 197 | "version": "0.23.0", 198 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", 199 | "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", 200 | "cpu": [ 201 | "loong64" 202 | ], 203 | "dev": true, 204 | "optional": true, 205 | "os": [ 206 | "linux" 207 | ], 208 | "engines": { 209 | "node": ">=18" 210 | } 211 | }, 212 | "node_modules/@esbuild/linux-mips64el": { 213 | "version": "0.23.0", 214 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", 215 | "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", 216 | "cpu": [ 217 | "mips64el" 218 | ], 219 | "dev": true, 220 | "optional": true, 221 | "os": [ 222 | "linux" 223 | ], 224 | "engines": { 225 | "node": ">=18" 226 | } 227 | }, 228 | "node_modules/@esbuild/linux-ppc64": { 229 | "version": "0.23.0", 230 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", 231 | "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", 232 | "cpu": [ 233 | "ppc64" 234 | ], 235 | "dev": true, 236 | "optional": true, 237 | "os": [ 238 | "linux" 239 | ], 240 | "engines": { 241 | "node": ">=18" 242 | } 243 | }, 244 | "node_modules/@esbuild/linux-riscv64": { 245 | "version": "0.23.0", 246 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", 247 | "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", 248 | "cpu": [ 249 | "riscv64" 250 | ], 251 | "dev": true, 252 | "optional": true, 253 | "os": [ 254 | "linux" 255 | ], 256 | "engines": { 257 | "node": ">=18" 258 | } 259 | }, 260 | "node_modules/@esbuild/linux-s390x": { 261 | "version": "0.23.0", 262 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", 263 | "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", 264 | "cpu": [ 265 | "s390x" 266 | ], 267 | "dev": true, 268 | "optional": true, 269 | "os": [ 270 | "linux" 271 | ], 272 | "engines": { 273 | "node": ">=18" 274 | } 275 | }, 276 | "node_modules/@esbuild/linux-x64": { 277 | "version": "0.23.0", 278 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", 279 | "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", 280 | "cpu": [ 281 | "x64" 282 | ], 283 | "dev": true, 284 | "optional": true, 285 | "os": [ 286 | "linux" 287 | ], 288 | "engines": { 289 | "node": ">=18" 290 | } 291 | }, 292 | "node_modules/@esbuild/netbsd-x64": { 293 | "version": "0.23.0", 294 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", 295 | "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", 296 | "cpu": [ 297 | "x64" 298 | ], 299 | "dev": true, 300 | "optional": true, 301 | "os": [ 302 | "netbsd" 303 | ], 304 | "engines": { 305 | "node": ">=18" 306 | } 307 | }, 308 | "node_modules/@esbuild/openbsd-arm64": { 309 | "version": "0.23.0", 310 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", 311 | "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", 312 | "cpu": [ 313 | "arm64" 314 | ], 315 | "dev": true, 316 | "optional": true, 317 | "os": [ 318 | "openbsd" 319 | ], 320 | "engines": { 321 | "node": ">=18" 322 | } 323 | }, 324 | "node_modules/@esbuild/openbsd-x64": { 325 | "version": "0.23.0", 326 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", 327 | "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", 328 | "cpu": [ 329 | "x64" 330 | ], 331 | "dev": true, 332 | "optional": true, 333 | "os": [ 334 | "openbsd" 335 | ], 336 | "engines": { 337 | "node": ">=18" 338 | } 339 | }, 340 | "node_modules/@esbuild/sunos-x64": { 341 | "version": "0.23.0", 342 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", 343 | "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", 344 | "cpu": [ 345 | "x64" 346 | ], 347 | "dev": true, 348 | "optional": true, 349 | "os": [ 350 | "sunos" 351 | ], 352 | "engines": { 353 | "node": ">=18" 354 | } 355 | }, 356 | "node_modules/@esbuild/win32-arm64": { 357 | "version": "0.23.0", 358 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", 359 | "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", 360 | "cpu": [ 361 | "arm64" 362 | ], 363 | "dev": true, 364 | "optional": true, 365 | "os": [ 366 | "win32" 367 | ], 368 | "engines": { 369 | "node": ">=18" 370 | } 371 | }, 372 | "node_modules/@esbuild/win32-ia32": { 373 | "version": "0.23.0", 374 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", 375 | "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", 376 | "cpu": [ 377 | "ia32" 378 | ], 379 | "dev": true, 380 | "optional": true, 381 | "os": [ 382 | "win32" 383 | ], 384 | "engines": { 385 | "node": ">=18" 386 | } 387 | }, 388 | "node_modules/@esbuild/win32-x64": { 389 | "version": "0.23.0", 390 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", 391 | "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", 392 | "cpu": [ 393 | "x64" 394 | ], 395 | "dev": true, 396 | "optional": true, 397 | "os": [ 398 | "win32" 399 | ], 400 | "engines": { 401 | "node": ">=18" 402 | } 403 | }, 404 | "node_modules/@playwright/test": { 405 | "version": "1.45.2", 406 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", 407 | "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", 408 | "dev": true, 409 | "dependencies": { 410 | "playwright": "1.45.2" 411 | }, 412 | "bin": { 413 | "playwright": "cli.js" 414 | }, 415 | "engines": { 416 | "node": ">=18" 417 | } 418 | }, 419 | "node_modules/accepts": { 420 | "version": "1.3.8", 421 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 422 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 423 | "dev": true, 424 | "dependencies": { 425 | "mime-types": "~2.1.34", 426 | "negotiator": "0.6.3" 427 | }, 428 | "engines": { 429 | "node": ">= 0.6" 430 | } 431 | }, 432 | "node_modules/array-flatten": { 433 | "version": "1.1.1", 434 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 435 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 436 | "dev": true 437 | }, 438 | "node_modules/body-parser": { 439 | "version": "1.20.2", 440 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 441 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 442 | "dev": true, 443 | "dependencies": { 444 | "bytes": "3.1.2", 445 | "content-type": "~1.0.5", 446 | "debug": "2.6.9", 447 | "depd": "2.0.0", 448 | "destroy": "1.2.0", 449 | "http-errors": "2.0.0", 450 | "iconv-lite": "0.4.24", 451 | "on-finished": "2.4.1", 452 | "qs": "6.11.0", 453 | "raw-body": "2.5.2", 454 | "type-is": "~1.6.18", 455 | "unpipe": "1.0.0" 456 | }, 457 | "engines": { 458 | "node": ">= 0.8", 459 | "npm": "1.2.8000 || >= 1.4.16" 460 | } 461 | }, 462 | "node_modules/bytes": { 463 | "version": "3.1.2", 464 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 465 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 466 | "dev": true, 467 | "engines": { 468 | "node": ">= 0.8" 469 | } 470 | }, 471 | "node_modules/call-bind": { 472 | "version": "1.0.7", 473 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 474 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 475 | "dev": true, 476 | "dependencies": { 477 | "es-define-property": "^1.0.0", 478 | "es-errors": "^1.3.0", 479 | "function-bind": "^1.1.2", 480 | "get-intrinsic": "^1.2.4", 481 | "set-function-length": "^1.2.1" 482 | }, 483 | "engines": { 484 | "node": ">= 0.4" 485 | }, 486 | "funding": { 487 | "url": "https://github.com/sponsors/ljharb" 488 | } 489 | }, 490 | "node_modules/content-disposition": { 491 | "version": "0.5.4", 492 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 493 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 494 | "dev": true, 495 | "dependencies": { 496 | "safe-buffer": "5.2.1" 497 | }, 498 | "engines": { 499 | "node": ">= 0.6" 500 | } 501 | }, 502 | "node_modules/content-type": { 503 | "version": "1.0.5", 504 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 505 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 506 | "dev": true, 507 | "engines": { 508 | "node": ">= 0.6" 509 | } 510 | }, 511 | "node_modules/cookie": { 512 | "version": "0.6.0", 513 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 514 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 515 | "dev": true, 516 | "engines": { 517 | "node": ">= 0.6" 518 | } 519 | }, 520 | "node_modules/cookie-signature": { 521 | "version": "1.0.6", 522 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 523 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 524 | "dev": true 525 | }, 526 | "node_modules/debug": { 527 | "version": "2.6.9", 528 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 529 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 530 | "dev": true, 531 | "dependencies": { 532 | "ms": "2.0.0" 533 | } 534 | }, 535 | "node_modules/define-data-property": { 536 | "version": "1.1.4", 537 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 538 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 539 | "dev": true, 540 | "dependencies": { 541 | "es-define-property": "^1.0.0", 542 | "es-errors": "^1.3.0", 543 | "gopd": "^1.0.1" 544 | }, 545 | "engines": { 546 | "node": ">= 0.4" 547 | }, 548 | "funding": { 549 | "url": "https://github.com/sponsors/ljharb" 550 | } 551 | }, 552 | "node_modules/depd": { 553 | "version": "2.0.0", 554 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 555 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 556 | "dev": true, 557 | "engines": { 558 | "node": ">= 0.8" 559 | } 560 | }, 561 | "node_modules/destroy": { 562 | "version": "1.2.0", 563 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 564 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 565 | "dev": true, 566 | "engines": { 567 | "node": ">= 0.8", 568 | "npm": "1.2.8000 || >= 1.4.16" 569 | } 570 | }, 571 | "node_modules/ee-first": { 572 | "version": "1.1.1", 573 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 574 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 575 | "dev": true 576 | }, 577 | "node_modules/encodeurl": { 578 | "version": "1.0.2", 579 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 580 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 581 | "dev": true, 582 | "engines": { 583 | "node": ">= 0.8" 584 | } 585 | }, 586 | "node_modules/es-define-property": { 587 | "version": "1.0.0", 588 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 589 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 590 | "dev": true, 591 | "dependencies": { 592 | "get-intrinsic": "^1.2.4" 593 | }, 594 | "engines": { 595 | "node": ">= 0.4" 596 | } 597 | }, 598 | "node_modules/es-errors": { 599 | "version": "1.3.0", 600 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 601 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 602 | "dev": true, 603 | "engines": { 604 | "node": ">= 0.4" 605 | } 606 | }, 607 | "node_modules/esbuild": { 608 | "version": "0.23.0", 609 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", 610 | "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", 611 | "dev": true, 612 | "hasInstallScript": true, 613 | "bin": { 614 | "esbuild": "bin/esbuild" 615 | }, 616 | "engines": { 617 | "node": ">=18" 618 | }, 619 | "optionalDependencies": { 620 | "@esbuild/aix-ppc64": "0.23.0", 621 | "@esbuild/android-arm": "0.23.0", 622 | "@esbuild/android-arm64": "0.23.0", 623 | "@esbuild/android-x64": "0.23.0", 624 | "@esbuild/darwin-arm64": "0.23.0", 625 | "@esbuild/darwin-x64": "0.23.0", 626 | "@esbuild/freebsd-arm64": "0.23.0", 627 | "@esbuild/freebsd-x64": "0.23.0", 628 | "@esbuild/linux-arm": "0.23.0", 629 | "@esbuild/linux-arm64": "0.23.0", 630 | "@esbuild/linux-ia32": "0.23.0", 631 | "@esbuild/linux-loong64": "0.23.0", 632 | "@esbuild/linux-mips64el": "0.23.0", 633 | "@esbuild/linux-ppc64": "0.23.0", 634 | "@esbuild/linux-riscv64": "0.23.0", 635 | "@esbuild/linux-s390x": "0.23.0", 636 | "@esbuild/linux-x64": "0.23.0", 637 | "@esbuild/netbsd-x64": "0.23.0", 638 | "@esbuild/openbsd-arm64": "0.23.0", 639 | "@esbuild/openbsd-x64": "0.23.0", 640 | "@esbuild/sunos-x64": "0.23.0", 641 | "@esbuild/win32-arm64": "0.23.0", 642 | "@esbuild/win32-ia32": "0.23.0", 643 | "@esbuild/win32-x64": "0.23.0" 644 | } 645 | }, 646 | "node_modules/escape-html": { 647 | "version": "1.0.3", 648 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 649 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 650 | "dev": true 651 | }, 652 | "node_modules/etag": { 653 | "version": "1.8.1", 654 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 655 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 656 | "dev": true, 657 | "engines": { 658 | "node": ">= 0.6" 659 | } 660 | }, 661 | "node_modules/express": { 662 | "version": "4.19.2", 663 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", 664 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", 665 | "dev": true, 666 | "dependencies": { 667 | "accepts": "~1.3.8", 668 | "array-flatten": "1.1.1", 669 | "body-parser": "1.20.2", 670 | "content-disposition": "0.5.4", 671 | "content-type": "~1.0.4", 672 | "cookie": "0.6.0", 673 | "cookie-signature": "1.0.6", 674 | "debug": "2.6.9", 675 | "depd": "2.0.0", 676 | "encodeurl": "~1.0.2", 677 | "escape-html": "~1.0.3", 678 | "etag": "~1.8.1", 679 | "finalhandler": "1.2.0", 680 | "fresh": "0.5.2", 681 | "http-errors": "2.0.0", 682 | "merge-descriptors": "1.0.1", 683 | "methods": "~1.1.2", 684 | "on-finished": "2.4.1", 685 | "parseurl": "~1.3.3", 686 | "path-to-regexp": "0.1.7", 687 | "proxy-addr": "~2.0.7", 688 | "qs": "6.11.0", 689 | "range-parser": "~1.2.1", 690 | "safe-buffer": "5.2.1", 691 | "send": "0.18.0", 692 | "serve-static": "1.15.0", 693 | "setprototypeof": "1.2.0", 694 | "statuses": "2.0.1", 695 | "type-is": "~1.6.18", 696 | "utils-merge": "1.0.1", 697 | "vary": "~1.1.2" 698 | }, 699 | "engines": { 700 | "node": ">= 0.10.0" 701 | } 702 | }, 703 | "node_modules/finalhandler": { 704 | "version": "1.2.0", 705 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 706 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 707 | "dev": true, 708 | "dependencies": { 709 | "debug": "2.6.9", 710 | "encodeurl": "~1.0.2", 711 | "escape-html": "~1.0.3", 712 | "on-finished": "2.4.1", 713 | "parseurl": "~1.3.3", 714 | "statuses": "2.0.1", 715 | "unpipe": "~1.0.0" 716 | }, 717 | "engines": { 718 | "node": ">= 0.8" 719 | } 720 | }, 721 | "node_modules/forwarded": { 722 | "version": "0.2.0", 723 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 724 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 725 | "dev": true, 726 | "engines": { 727 | "node": ">= 0.6" 728 | } 729 | }, 730 | "node_modules/fresh": { 731 | "version": "0.5.2", 732 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 733 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 734 | "dev": true, 735 | "engines": { 736 | "node": ">= 0.6" 737 | } 738 | }, 739 | "node_modules/fsevents": { 740 | "version": "2.3.2", 741 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 742 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 743 | "dev": true, 744 | "hasInstallScript": true, 745 | "optional": true, 746 | "os": [ 747 | "darwin" 748 | ], 749 | "engines": { 750 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 751 | } 752 | }, 753 | "node_modules/function-bind": { 754 | "version": "1.1.2", 755 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 756 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 757 | "dev": true, 758 | "funding": { 759 | "url": "https://github.com/sponsors/ljharb" 760 | } 761 | }, 762 | "node_modules/get-intrinsic": { 763 | "version": "1.2.4", 764 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 765 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 766 | "dev": true, 767 | "dependencies": { 768 | "es-errors": "^1.3.0", 769 | "function-bind": "^1.1.2", 770 | "has-proto": "^1.0.1", 771 | "has-symbols": "^1.0.3", 772 | "hasown": "^2.0.0" 773 | }, 774 | "engines": { 775 | "node": ">= 0.4" 776 | }, 777 | "funding": { 778 | "url": "https://github.com/sponsors/ljharb" 779 | } 780 | }, 781 | "node_modules/gopd": { 782 | "version": "1.0.1", 783 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 784 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 785 | "dev": true, 786 | "dependencies": { 787 | "get-intrinsic": "^1.1.3" 788 | }, 789 | "funding": { 790 | "url": "https://github.com/sponsors/ljharb" 791 | } 792 | }, 793 | "node_modules/has-property-descriptors": { 794 | "version": "1.0.2", 795 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 796 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 797 | "dev": true, 798 | "dependencies": { 799 | "es-define-property": "^1.0.0" 800 | }, 801 | "funding": { 802 | "url": "https://github.com/sponsors/ljharb" 803 | } 804 | }, 805 | "node_modules/has-proto": { 806 | "version": "1.0.3", 807 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 808 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 809 | "dev": true, 810 | "engines": { 811 | "node": ">= 0.4" 812 | }, 813 | "funding": { 814 | "url": "https://github.com/sponsors/ljharb" 815 | } 816 | }, 817 | "node_modules/has-symbols": { 818 | "version": "1.0.3", 819 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 820 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 821 | "dev": true, 822 | "engines": { 823 | "node": ">= 0.4" 824 | }, 825 | "funding": { 826 | "url": "https://github.com/sponsors/ljharb" 827 | } 828 | }, 829 | "node_modules/hasown": { 830 | "version": "2.0.2", 831 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 832 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 833 | "dev": true, 834 | "dependencies": { 835 | "function-bind": "^1.1.2" 836 | }, 837 | "engines": { 838 | "node": ">= 0.4" 839 | } 840 | }, 841 | "node_modules/http-errors": { 842 | "version": "2.0.0", 843 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 844 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 845 | "dev": true, 846 | "dependencies": { 847 | "depd": "2.0.0", 848 | "inherits": "2.0.4", 849 | "setprototypeof": "1.2.0", 850 | "statuses": "2.0.1", 851 | "toidentifier": "1.0.1" 852 | }, 853 | "engines": { 854 | "node": ">= 0.8" 855 | } 856 | }, 857 | "node_modules/iconv-lite": { 858 | "version": "0.4.24", 859 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 860 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 861 | "dev": true, 862 | "dependencies": { 863 | "safer-buffer": ">= 2.1.2 < 3" 864 | }, 865 | "engines": { 866 | "node": ">=0.10.0" 867 | } 868 | }, 869 | "node_modules/inherits": { 870 | "version": "2.0.4", 871 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 872 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 873 | "dev": true 874 | }, 875 | "node_modules/ipaddr.js": { 876 | "version": "1.9.1", 877 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 878 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 879 | "dev": true, 880 | "engines": { 881 | "node": ">= 0.10" 882 | } 883 | }, 884 | "node_modules/media-typer": { 885 | "version": "0.3.0", 886 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 887 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 888 | "dev": true, 889 | "engines": { 890 | "node": ">= 0.6" 891 | } 892 | }, 893 | "node_modules/merge-descriptors": { 894 | "version": "1.0.1", 895 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 896 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", 897 | "dev": true 898 | }, 899 | "node_modules/methods": { 900 | "version": "1.1.2", 901 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 902 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 903 | "dev": true, 904 | "engines": { 905 | "node": ">= 0.6" 906 | } 907 | }, 908 | "node_modules/mime": { 909 | "version": "1.6.0", 910 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 911 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 912 | "dev": true, 913 | "bin": { 914 | "mime": "cli.js" 915 | }, 916 | "engines": { 917 | "node": ">=4" 918 | } 919 | }, 920 | "node_modules/mime-db": { 921 | "version": "1.52.0", 922 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 923 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 924 | "dev": true, 925 | "engines": { 926 | "node": ">= 0.6" 927 | } 928 | }, 929 | "node_modules/mime-types": { 930 | "version": "2.1.35", 931 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 932 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 933 | "dev": true, 934 | "dependencies": { 935 | "mime-db": "1.52.0" 936 | }, 937 | "engines": { 938 | "node": ">= 0.6" 939 | } 940 | }, 941 | "node_modules/ms": { 942 | "version": "2.0.0", 943 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 944 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 945 | "dev": true 946 | }, 947 | "node_modules/negotiator": { 948 | "version": "0.6.3", 949 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 950 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 951 | "dev": true, 952 | "engines": { 953 | "node": ">= 0.6" 954 | } 955 | }, 956 | "node_modules/object-inspect": { 957 | "version": "1.13.2", 958 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 959 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 960 | "dev": true, 961 | "engines": { 962 | "node": ">= 0.4" 963 | }, 964 | "funding": { 965 | "url": "https://github.com/sponsors/ljharb" 966 | } 967 | }, 968 | "node_modules/on-finished": { 969 | "version": "2.4.1", 970 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 971 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 972 | "dev": true, 973 | "dependencies": { 974 | "ee-first": "1.1.1" 975 | }, 976 | "engines": { 977 | "node": ">= 0.8" 978 | } 979 | }, 980 | "node_modules/parseurl": { 981 | "version": "1.3.3", 982 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 983 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 984 | "dev": true, 985 | "engines": { 986 | "node": ">= 0.8" 987 | } 988 | }, 989 | "node_modules/path-to-regexp": { 990 | "version": "0.1.7", 991 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 992 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", 993 | "dev": true 994 | }, 995 | "node_modules/playwright": { 996 | "version": "1.45.2", 997 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", 998 | "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", 999 | "dev": true, 1000 | "dependencies": { 1001 | "playwright-core": "1.45.2" 1002 | }, 1003 | "bin": { 1004 | "playwright": "cli.js" 1005 | }, 1006 | "engines": { 1007 | "node": ">=18" 1008 | }, 1009 | "optionalDependencies": { 1010 | "fsevents": "2.3.2" 1011 | } 1012 | }, 1013 | "node_modules/playwright-core": { 1014 | "version": "1.45.2", 1015 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", 1016 | "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", 1017 | "dev": true, 1018 | "bin": { 1019 | "playwright-core": "cli.js" 1020 | }, 1021 | "engines": { 1022 | "node": ">=18" 1023 | } 1024 | }, 1025 | "node_modules/prettier": { 1026 | "version": "3.3.3", 1027 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", 1028 | "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", 1029 | "dev": true, 1030 | "bin": { 1031 | "prettier": "bin/prettier.cjs" 1032 | }, 1033 | "engines": { 1034 | "node": ">=14" 1035 | }, 1036 | "funding": { 1037 | "url": "https://github.com/prettier/prettier?sponsor=1" 1038 | } 1039 | }, 1040 | "node_modules/proxy-addr": { 1041 | "version": "2.0.7", 1042 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1043 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1044 | "dev": true, 1045 | "dependencies": { 1046 | "forwarded": "0.2.0", 1047 | "ipaddr.js": "1.9.1" 1048 | }, 1049 | "engines": { 1050 | "node": ">= 0.10" 1051 | } 1052 | }, 1053 | "node_modules/qs": { 1054 | "version": "6.11.0", 1055 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 1056 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 1057 | "dev": true, 1058 | "dependencies": { 1059 | "side-channel": "^1.0.4" 1060 | }, 1061 | "engines": { 1062 | "node": ">=0.6" 1063 | }, 1064 | "funding": { 1065 | "url": "https://github.com/sponsors/ljharb" 1066 | } 1067 | }, 1068 | "node_modules/range-parser": { 1069 | "version": "1.2.1", 1070 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1071 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1072 | "dev": true, 1073 | "engines": { 1074 | "node": ">= 0.6" 1075 | } 1076 | }, 1077 | "node_modules/raw-body": { 1078 | "version": "2.5.2", 1079 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1080 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1081 | "dev": true, 1082 | "dependencies": { 1083 | "bytes": "3.1.2", 1084 | "http-errors": "2.0.0", 1085 | "iconv-lite": "0.4.24", 1086 | "unpipe": "1.0.0" 1087 | }, 1088 | "engines": { 1089 | "node": ">= 0.8" 1090 | } 1091 | }, 1092 | "node_modules/safe-buffer": { 1093 | "version": "5.2.1", 1094 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1095 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1096 | "dev": true, 1097 | "funding": [ 1098 | { 1099 | "type": "github", 1100 | "url": "https://github.com/sponsors/feross" 1101 | }, 1102 | { 1103 | "type": "patreon", 1104 | "url": "https://www.patreon.com/feross" 1105 | }, 1106 | { 1107 | "type": "consulting", 1108 | "url": "https://feross.org/support" 1109 | } 1110 | ] 1111 | }, 1112 | "node_modules/safer-buffer": { 1113 | "version": "2.1.2", 1114 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1115 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1116 | "dev": true 1117 | }, 1118 | "node_modules/send": { 1119 | "version": "0.18.0", 1120 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 1121 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 1122 | "dev": true, 1123 | "dependencies": { 1124 | "debug": "2.6.9", 1125 | "depd": "2.0.0", 1126 | "destroy": "1.2.0", 1127 | "encodeurl": "~1.0.2", 1128 | "escape-html": "~1.0.3", 1129 | "etag": "~1.8.1", 1130 | "fresh": "0.5.2", 1131 | "http-errors": "2.0.0", 1132 | "mime": "1.6.0", 1133 | "ms": "2.1.3", 1134 | "on-finished": "2.4.1", 1135 | "range-parser": "~1.2.1", 1136 | "statuses": "2.0.1" 1137 | }, 1138 | "engines": { 1139 | "node": ">= 0.8.0" 1140 | } 1141 | }, 1142 | "node_modules/send/node_modules/ms": { 1143 | "version": "2.1.3", 1144 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1145 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1146 | "dev": true 1147 | }, 1148 | "node_modules/serve-static": { 1149 | "version": "1.15.0", 1150 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1151 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1152 | "dev": true, 1153 | "dependencies": { 1154 | "encodeurl": "~1.0.2", 1155 | "escape-html": "~1.0.3", 1156 | "parseurl": "~1.3.3", 1157 | "send": "0.18.0" 1158 | }, 1159 | "engines": { 1160 | "node": ">= 0.8.0" 1161 | } 1162 | }, 1163 | "node_modules/set-function-length": { 1164 | "version": "1.2.2", 1165 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1166 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1167 | "dev": true, 1168 | "dependencies": { 1169 | "define-data-property": "^1.1.4", 1170 | "es-errors": "^1.3.0", 1171 | "function-bind": "^1.1.2", 1172 | "get-intrinsic": "^1.2.4", 1173 | "gopd": "^1.0.1", 1174 | "has-property-descriptors": "^1.0.2" 1175 | }, 1176 | "engines": { 1177 | "node": ">= 0.4" 1178 | } 1179 | }, 1180 | "node_modules/setprototypeof": { 1181 | "version": "1.2.0", 1182 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1183 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1184 | "dev": true 1185 | }, 1186 | "node_modules/side-channel": { 1187 | "version": "1.0.6", 1188 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 1189 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 1190 | "dev": true, 1191 | "dependencies": { 1192 | "call-bind": "^1.0.7", 1193 | "es-errors": "^1.3.0", 1194 | "get-intrinsic": "^1.2.4", 1195 | "object-inspect": "^1.13.1" 1196 | }, 1197 | "engines": { 1198 | "node": ">= 0.4" 1199 | }, 1200 | "funding": { 1201 | "url": "https://github.com/sponsors/ljharb" 1202 | } 1203 | }, 1204 | "node_modules/statuses": { 1205 | "version": "2.0.1", 1206 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1207 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1208 | "dev": true, 1209 | "engines": { 1210 | "node": ">= 0.8" 1211 | } 1212 | }, 1213 | "node_modules/toidentifier": { 1214 | "version": "1.0.1", 1215 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1216 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1217 | "dev": true, 1218 | "engines": { 1219 | "node": ">=0.6" 1220 | } 1221 | }, 1222 | "node_modules/type-is": { 1223 | "version": "1.6.18", 1224 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1225 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1226 | "dev": true, 1227 | "dependencies": { 1228 | "media-typer": "0.3.0", 1229 | "mime-types": "~2.1.24" 1230 | }, 1231 | "engines": { 1232 | "node": ">= 0.6" 1233 | } 1234 | }, 1235 | "node_modules/typescript": { 1236 | "version": "5.5.4", 1237 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", 1238 | "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", 1239 | "dev": true, 1240 | "bin": { 1241 | "tsc": "bin/tsc", 1242 | "tsserver": "bin/tsserver" 1243 | }, 1244 | "engines": { 1245 | "node": ">=14.17" 1246 | } 1247 | }, 1248 | "node_modules/unpipe": { 1249 | "version": "1.0.0", 1250 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1251 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1252 | "dev": true, 1253 | "engines": { 1254 | "node": ">= 0.8" 1255 | } 1256 | }, 1257 | "node_modules/utils-merge": { 1258 | "version": "1.0.1", 1259 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1260 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1261 | "dev": true, 1262 | "engines": { 1263 | "node": ">= 0.4.0" 1264 | } 1265 | }, 1266 | "node_modules/vary": { 1267 | "version": "1.1.2", 1268 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1269 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1270 | "dev": true, 1271 | "engines": { 1272 | "node": ">= 0.8" 1273 | } 1274 | } 1275 | }, 1276 | "dependencies": { 1277 | "@esbuild/aix-ppc64": { 1278 | "version": "0.23.0", 1279 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", 1280 | "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", 1281 | "dev": true, 1282 | "optional": true 1283 | }, 1284 | "@esbuild/android-arm": { 1285 | "version": "0.23.0", 1286 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", 1287 | "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", 1288 | "dev": true, 1289 | "optional": true 1290 | }, 1291 | "@esbuild/android-arm64": { 1292 | "version": "0.23.0", 1293 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", 1294 | "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", 1295 | "dev": true, 1296 | "optional": true 1297 | }, 1298 | "@esbuild/android-x64": { 1299 | "version": "0.23.0", 1300 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", 1301 | "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", 1302 | "dev": true, 1303 | "optional": true 1304 | }, 1305 | "@esbuild/darwin-arm64": { 1306 | "version": "0.23.0", 1307 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", 1308 | "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", 1309 | "dev": true, 1310 | "optional": true 1311 | }, 1312 | "@esbuild/darwin-x64": { 1313 | "version": "0.23.0", 1314 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", 1315 | "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", 1316 | "dev": true, 1317 | "optional": true 1318 | }, 1319 | "@esbuild/freebsd-arm64": { 1320 | "version": "0.23.0", 1321 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", 1322 | "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", 1323 | "dev": true, 1324 | "optional": true 1325 | }, 1326 | "@esbuild/freebsd-x64": { 1327 | "version": "0.23.0", 1328 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", 1329 | "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", 1330 | "dev": true, 1331 | "optional": true 1332 | }, 1333 | "@esbuild/linux-arm": { 1334 | "version": "0.23.0", 1335 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", 1336 | "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", 1337 | "dev": true, 1338 | "optional": true 1339 | }, 1340 | "@esbuild/linux-arm64": { 1341 | "version": "0.23.0", 1342 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", 1343 | "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", 1344 | "dev": true, 1345 | "optional": true 1346 | }, 1347 | "@esbuild/linux-ia32": { 1348 | "version": "0.23.0", 1349 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", 1350 | "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", 1351 | "dev": true, 1352 | "optional": true 1353 | }, 1354 | "@esbuild/linux-loong64": { 1355 | "version": "0.23.0", 1356 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", 1357 | "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", 1358 | "dev": true, 1359 | "optional": true 1360 | }, 1361 | "@esbuild/linux-mips64el": { 1362 | "version": "0.23.0", 1363 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", 1364 | "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", 1365 | "dev": true, 1366 | "optional": true 1367 | }, 1368 | "@esbuild/linux-ppc64": { 1369 | "version": "0.23.0", 1370 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", 1371 | "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", 1372 | "dev": true, 1373 | "optional": true 1374 | }, 1375 | "@esbuild/linux-riscv64": { 1376 | "version": "0.23.0", 1377 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", 1378 | "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", 1379 | "dev": true, 1380 | "optional": true 1381 | }, 1382 | "@esbuild/linux-s390x": { 1383 | "version": "0.23.0", 1384 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", 1385 | "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", 1386 | "dev": true, 1387 | "optional": true 1388 | }, 1389 | "@esbuild/linux-x64": { 1390 | "version": "0.23.0", 1391 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", 1392 | "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", 1393 | "dev": true, 1394 | "optional": true 1395 | }, 1396 | "@esbuild/netbsd-x64": { 1397 | "version": "0.23.0", 1398 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", 1399 | "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", 1400 | "dev": true, 1401 | "optional": true 1402 | }, 1403 | "@esbuild/openbsd-arm64": { 1404 | "version": "0.23.0", 1405 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", 1406 | "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", 1407 | "dev": true, 1408 | "optional": true 1409 | }, 1410 | "@esbuild/openbsd-x64": { 1411 | "version": "0.23.0", 1412 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", 1413 | "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", 1414 | "dev": true, 1415 | "optional": true 1416 | }, 1417 | "@esbuild/sunos-x64": { 1418 | "version": "0.23.0", 1419 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", 1420 | "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", 1421 | "dev": true, 1422 | "optional": true 1423 | }, 1424 | "@esbuild/win32-arm64": { 1425 | "version": "0.23.0", 1426 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", 1427 | "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", 1428 | "dev": true, 1429 | "optional": true 1430 | }, 1431 | "@esbuild/win32-ia32": { 1432 | "version": "0.23.0", 1433 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", 1434 | "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", 1435 | "dev": true, 1436 | "optional": true 1437 | }, 1438 | "@esbuild/win32-x64": { 1439 | "version": "0.23.0", 1440 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", 1441 | "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", 1442 | "dev": true, 1443 | "optional": true 1444 | }, 1445 | "@playwright/test": { 1446 | "version": "1.45.2", 1447 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.2.tgz", 1448 | "integrity": "sha512-JxG9eq92ET75EbVi3s+4sYbcG7q72ECeZNbdBlaMkGcNbiDQ4cAi8U2QP5oKkOx+1gpaiL1LDStmzCaEM1Z6fQ==", 1449 | "dev": true, 1450 | "requires": { 1451 | "playwright": "1.45.2" 1452 | } 1453 | }, 1454 | "accepts": { 1455 | "version": "1.3.8", 1456 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 1457 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 1458 | "dev": true, 1459 | "requires": { 1460 | "mime-types": "~2.1.34", 1461 | "negotiator": "0.6.3" 1462 | } 1463 | }, 1464 | "array-flatten": { 1465 | "version": "1.1.1", 1466 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 1467 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 1468 | "dev": true 1469 | }, 1470 | "body-parser": { 1471 | "version": "1.20.2", 1472 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 1473 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 1474 | "dev": true, 1475 | "requires": { 1476 | "bytes": "3.1.2", 1477 | "content-type": "~1.0.5", 1478 | "debug": "2.6.9", 1479 | "depd": "2.0.0", 1480 | "destroy": "1.2.0", 1481 | "http-errors": "2.0.0", 1482 | "iconv-lite": "0.4.24", 1483 | "on-finished": "2.4.1", 1484 | "qs": "6.11.0", 1485 | "raw-body": "2.5.2", 1486 | "type-is": "~1.6.18", 1487 | "unpipe": "1.0.0" 1488 | } 1489 | }, 1490 | "bytes": { 1491 | "version": "3.1.2", 1492 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 1493 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 1494 | "dev": true 1495 | }, 1496 | "call-bind": { 1497 | "version": "1.0.7", 1498 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 1499 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 1500 | "dev": true, 1501 | "requires": { 1502 | "es-define-property": "^1.0.0", 1503 | "es-errors": "^1.3.0", 1504 | "function-bind": "^1.1.2", 1505 | "get-intrinsic": "^1.2.4", 1506 | "set-function-length": "^1.2.1" 1507 | } 1508 | }, 1509 | "content-disposition": { 1510 | "version": "0.5.4", 1511 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 1512 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 1513 | "dev": true, 1514 | "requires": { 1515 | "safe-buffer": "5.2.1" 1516 | } 1517 | }, 1518 | "content-type": { 1519 | "version": "1.0.5", 1520 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 1521 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 1522 | "dev": true 1523 | }, 1524 | "cookie": { 1525 | "version": "0.6.0", 1526 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", 1527 | "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", 1528 | "dev": true 1529 | }, 1530 | "cookie-signature": { 1531 | "version": "1.0.6", 1532 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 1533 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 1534 | "dev": true 1535 | }, 1536 | "debug": { 1537 | "version": "2.6.9", 1538 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1539 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1540 | "dev": true, 1541 | "requires": { 1542 | "ms": "2.0.0" 1543 | } 1544 | }, 1545 | "define-data-property": { 1546 | "version": "1.1.4", 1547 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 1548 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 1549 | "dev": true, 1550 | "requires": { 1551 | "es-define-property": "^1.0.0", 1552 | "es-errors": "^1.3.0", 1553 | "gopd": "^1.0.1" 1554 | } 1555 | }, 1556 | "depd": { 1557 | "version": "2.0.0", 1558 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 1559 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 1560 | "dev": true 1561 | }, 1562 | "destroy": { 1563 | "version": "1.2.0", 1564 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 1565 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 1566 | "dev": true 1567 | }, 1568 | "ee-first": { 1569 | "version": "1.1.1", 1570 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1571 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 1572 | "dev": true 1573 | }, 1574 | "encodeurl": { 1575 | "version": "1.0.2", 1576 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1577 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1578 | "dev": true 1579 | }, 1580 | "es-define-property": { 1581 | "version": "1.0.0", 1582 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 1583 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 1584 | "dev": true, 1585 | "requires": { 1586 | "get-intrinsic": "^1.2.4" 1587 | } 1588 | }, 1589 | "es-errors": { 1590 | "version": "1.3.0", 1591 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 1592 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 1593 | "dev": true 1594 | }, 1595 | "esbuild": { 1596 | "version": "0.23.0", 1597 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", 1598 | "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", 1599 | "dev": true, 1600 | "requires": { 1601 | "@esbuild/aix-ppc64": "0.23.0", 1602 | "@esbuild/android-arm": "0.23.0", 1603 | "@esbuild/android-arm64": "0.23.0", 1604 | "@esbuild/android-x64": "0.23.0", 1605 | "@esbuild/darwin-arm64": "0.23.0", 1606 | "@esbuild/darwin-x64": "0.23.0", 1607 | "@esbuild/freebsd-arm64": "0.23.0", 1608 | "@esbuild/freebsd-x64": "0.23.0", 1609 | "@esbuild/linux-arm": "0.23.0", 1610 | "@esbuild/linux-arm64": "0.23.0", 1611 | "@esbuild/linux-ia32": "0.23.0", 1612 | "@esbuild/linux-loong64": "0.23.0", 1613 | "@esbuild/linux-mips64el": "0.23.0", 1614 | "@esbuild/linux-ppc64": "0.23.0", 1615 | "@esbuild/linux-riscv64": "0.23.0", 1616 | "@esbuild/linux-s390x": "0.23.0", 1617 | "@esbuild/linux-x64": "0.23.0", 1618 | "@esbuild/netbsd-x64": "0.23.0", 1619 | "@esbuild/openbsd-arm64": "0.23.0", 1620 | "@esbuild/openbsd-x64": "0.23.0", 1621 | "@esbuild/sunos-x64": "0.23.0", 1622 | "@esbuild/win32-arm64": "0.23.0", 1623 | "@esbuild/win32-ia32": "0.23.0", 1624 | "@esbuild/win32-x64": "0.23.0" 1625 | } 1626 | }, 1627 | "escape-html": { 1628 | "version": "1.0.3", 1629 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1630 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 1631 | "dev": true 1632 | }, 1633 | "etag": { 1634 | "version": "1.8.1", 1635 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 1636 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 1637 | "dev": true 1638 | }, 1639 | "express": { 1640 | "version": "4.19.2", 1641 | "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", 1642 | "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", 1643 | "dev": true, 1644 | "requires": { 1645 | "accepts": "~1.3.8", 1646 | "array-flatten": "1.1.1", 1647 | "body-parser": "1.20.2", 1648 | "content-disposition": "0.5.4", 1649 | "content-type": "~1.0.4", 1650 | "cookie": "0.6.0", 1651 | "cookie-signature": "1.0.6", 1652 | "debug": "2.6.9", 1653 | "depd": "2.0.0", 1654 | "encodeurl": "~1.0.2", 1655 | "escape-html": "~1.0.3", 1656 | "etag": "~1.8.1", 1657 | "finalhandler": "1.2.0", 1658 | "fresh": "0.5.2", 1659 | "http-errors": "2.0.0", 1660 | "merge-descriptors": "1.0.1", 1661 | "methods": "~1.1.2", 1662 | "on-finished": "2.4.1", 1663 | "parseurl": "~1.3.3", 1664 | "path-to-regexp": "0.1.7", 1665 | "proxy-addr": "~2.0.7", 1666 | "qs": "6.11.0", 1667 | "range-parser": "~1.2.1", 1668 | "safe-buffer": "5.2.1", 1669 | "send": "0.18.0", 1670 | "serve-static": "1.15.0", 1671 | "setprototypeof": "1.2.0", 1672 | "statuses": "2.0.1", 1673 | "type-is": "~1.6.18", 1674 | "utils-merge": "1.0.1", 1675 | "vary": "~1.1.2" 1676 | } 1677 | }, 1678 | "finalhandler": { 1679 | "version": "1.2.0", 1680 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 1681 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 1682 | "dev": true, 1683 | "requires": { 1684 | "debug": "2.6.9", 1685 | "encodeurl": "~1.0.2", 1686 | "escape-html": "~1.0.3", 1687 | "on-finished": "2.4.1", 1688 | "parseurl": "~1.3.3", 1689 | "statuses": "2.0.1", 1690 | "unpipe": "~1.0.0" 1691 | } 1692 | }, 1693 | "forwarded": { 1694 | "version": "0.2.0", 1695 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 1696 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 1697 | "dev": true 1698 | }, 1699 | "fresh": { 1700 | "version": "0.5.2", 1701 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 1702 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 1703 | "dev": true 1704 | }, 1705 | "fsevents": { 1706 | "version": "2.3.2", 1707 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1708 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1709 | "dev": true, 1710 | "optional": true 1711 | }, 1712 | "function-bind": { 1713 | "version": "1.1.2", 1714 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 1715 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 1716 | "dev": true 1717 | }, 1718 | "get-intrinsic": { 1719 | "version": "1.2.4", 1720 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 1721 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 1722 | "dev": true, 1723 | "requires": { 1724 | "es-errors": "^1.3.0", 1725 | "function-bind": "^1.1.2", 1726 | "has-proto": "^1.0.1", 1727 | "has-symbols": "^1.0.3", 1728 | "hasown": "^2.0.0" 1729 | } 1730 | }, 1731 | "gopd": { 1732 | "version": "1.0.1", 1733 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 1734 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 1735 | "dev": true, 1736 | "requires": { 1737 | "get-intrinsic": "^1.1.3" 1738 | } 1739 | }, 1740 | "has-property-descriptors": { 1741 | "version": "1.0.2", 1742 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 1743 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 1744 | "dev": true, 1745 | "requires": { 1746 | "es-define-property": "^1.0.0" 1747 | } 1748 | }, 1749 | "has-proto": { 1750 | "version": "1.0.3", 1751 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 1752 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 1753 | "dev": true 1754 | }, 1755 | "has-symbols": { 1756 | "version": "1.0.3", 1757 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 1758 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 1759 | "dev": true 1760 | }, 1761 | "hasown": { 1762 | "version": "2.0.2", 1763 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1764 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1765 | "dev": true, 1766 | "requires": { 1767 | "function-bind": "^1.1.2" 1768 | } 1769 | }, 1770 | "http-errors": { 1771 | "version": "2.0.0", 1772 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1773 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1774 | "dev": true, 1775 | "requires": { 1776 | "depd": "2.0.0", 1777 | "inherits": "2.0.4", 1778 | "setprototypeof": "1.2.0", 1779 | "statuses": "2.0.1", 1780 | "toidentifier": "1.0.1" 1781 | } 1782 | }, 1783 | "iconv-lite": { 1784 | "version": "0.4.24", 1785 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1786 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1787 | "dev": true, 1788 | "requires": { 1789 | "safer-buffer": ">= 2.1.2 < 3" 1790 | } 1791 | }, 1792 | "inherits": { 1793 | "version": "2.0.4", 1794 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1795 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1796 | "dev": true 1797 | }, 1798 | "ipaddr.js": { 1799 | "version": "1.9.1", 1800 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1801 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 1802 | "dev": true 1803 | }, 1804 | "media-typer": { 1805 | "version": "0.3.0", 1806 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1807 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 1808 | "dev": true 1809 | }, 1810 | "merge-descriptors": { 1811 | "version": "1.0.1", 1812 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1813 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", 1814 | "dev": true 1815 | }, 1816 | "methods": { 1817 | "version": "1.1.2", 1818 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1819 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 1820 | "dev": true 1821 | }, 1822 | "mime": { 1823 | "version": "1.6.0", 1824 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1825 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1826 | "dev": true 1827 | }, 1828 | "mime-db": { 1829 | "version": "1.52.0", 1830 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1831 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1832 | "dev": true 1833 | }, 1834 | "mime-types": { 1835 | "version": "2.1.35", 1836 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1837 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1838 | "dev": true, 1839 | "requires": { 1840 | "mime-db": "1.52.0" 1841 | } 1842 | }, 1843 | "ms": { 1844 | "version": "2.0.0", 1845 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1846 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 1847 | "dev": true 1848 | }, 1849 | "negotiator": { 1850 | "version": "0.6.3", 1851 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1852 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1853 | "dev": true 1854 | }, 1855 | "object-inspect": { 1856 | "version": "1.13.2", 1857 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", 1858 | "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", 1859 | "dev": true 1860 | }, 1861 | "on-finished": { 1862 | "version": "2.4.1", 1863 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1864 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1865 | "dev": true, 1866 | "requires": { 1867 | "ee-first": "1.1.1" 1868 | } 1869 | }, 1870 | "parseurl": { 1871 | "version": "1.3.3", 1872 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1873 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1874 | "dev": true 1875 | }, 1876 | "path-to-regexp": { 1877 | "version": "0.1.7", 1878 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1879 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", 1880 | "dev": true 1881 | }, 1882 | "playwright": { 1883 | "version": "1.45.2", 1884 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.2.tgz", 1885 | "integrity": "sha512-ReywF2t/0teRvNBpfIgh5e4wnrI/8Su8ssdo5XsQKpjxJj+jspm00jSoz9BTg91TT0c9HRjXO7LBNVrgYj9X0g==", 1886 | "dev": true, 1887 | "requires": { 1888 | "fsevents": "2.3.2", 1889 | "playwright-core": "1.45.2" 1890 | } 1891 | }, 1892 | "playwright-core": { 1893 | "version": "1.45.2", 1894 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.2.tgz", 1895 | "integrity": "sha512-ha175tAWb0dTK0X4orvBIqi3jGEt701SMxMhyujxNrgd8K0Uy5wMSwwcQHtyB4om7INUkfndx02XnQ2p6dvLDw==", 1896 | "dev": true 1897 | }, 1898 | "prettier": { 1899 | "version": "3.3.3", 1900 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", 1901 | "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", 1902 | "dev": true 1903 | }, 1904 | "proxy-addr": { 1905 | "version": "2.0.7", 1906 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1907 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1908 | "dev": true, 1909 | "requires": { 1910 | "forwarded": "0.2.0", 1911 | "ipaddr.js": "1.9.1" 1912 | } 1913 | }, 1914 | "qs": { 1915 | "version": "6.11.0", 1916 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 1917 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 1918 | "dev": true, 1919 | "requires": { 1920 | "side-channel": "^1.0.4" 1921 | } 1922 | }, 1923 | "range-parser": { 1924 | "version": "1.2.1", 1925 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1926 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1927 | "dev": true 1928 | }, 1929 | "raw-body": { 1930 | "version": "2.5.2", 1931 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 1932 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 1933 | "dev": true, 1934 | "requires": { 1935 | "bytes": "3.1.2", 1936 | "http-errors": "2.0.0", 1937 | "iconv-lite": "0.4.24", 1938 | "unpipe": "1.0.0" 1939 | } 1940 | }, 1941 | "safe-buffer": { 1942 | "version": "5.2.1", 1943 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1944 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1945 | "dev": true 1946 | }, 1947 | "safer-buffer": { 1948 | "version": "2.1.2", 1949 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1950 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1951 | "dev": true 1952 | }, 1953 | "send": { 1954 | "version": "0.18.0", 1955 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 1956 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 1957 | "dev": true, 1958 | "requires": { 1959 | "debug": "2.6.9", 1960 | "depd": "2.0.0", 1961 | "destroy": "1.2.0", 1962 | "encodeurl": "~1.0.2", 1963 | "escape-html": "~1.0.3", 1964 | "etag": "~1.8.1", 1965 | "fresh": "0.5.2", 1966 | "http-errors": "2.0.0", 1967 | "mime": "1.6.0", 1968 | "ms": "2.1.3", 1969 | "on-finished": "2.4.1", 1970 | "range-parser": "~1.2.1", 1971 | "statuses": "2.0.1" 1972 | }, 1973 | "dependencies": { 1974 | "ms": { 1975 | "version": "2.1.3", 1976 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1977 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1978 | "dev": true 1979 | } 1980 | } 1981 | }, 1982 | "serve-static": { 1983 | "version": "1.15.0", 1984 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 1985 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 1986 | "dev": true, 1987 | "requires": { 1988 | "encodeurl": "~1.0.2", 1989 | "escape-html": "~1.0.3", 1990 | "parseurl": "~1.3.3", 1991 | "send": "0.18.0" 1992 | } 1993 | }, 1994 | "set-function-length": { 1995 | "version": "1.2.2", 1996 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 1997 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 1998 | "dev": true, 1999 | "requires": { 2000 | "define-data-property": "^1.1.4", 2001 | "es-errors": "^1.3.0", 2002 | "function-bind": "^1.1.2", 2003 | "get-intrinsic": "^1.2.4", 2004 | "gopd": "^1.0.1", 2005 | "has-property-descriptors": "^1.0.2" 2006 | } 2007 | }, 2008 | "setprototypeof": { 2009 | "version": "1.2.0", 2010 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 2011 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 2012 | "dev": true 2013 | }, 2014 | "side-channel": { 2015 | "version": "1.0.6", 2016 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 2017 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 2018 | "dev": true, 2019 | "requires": { 2020 | "call-bind": "^1.0.7", 2021 | "es-errors": "^1.3.0", 2022 | "get-intrinsic": "^1.2.4", 2023 | "object-inspect": "^1.13.1" 2024 | } 2025 | }, 2026 | "statuses": { 2027 | "version": "2.0.1", 2028 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 2029 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 2030 | "dev": true 2031 | }, 2032 | "toidentifier": { 2033 | "version": "1.0.1", 2034 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 2035 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 2036 | "dev": true 2037 | }, 2038 | "type-is": { 2039 | "version": "1.6.18", 2040 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 2041 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 2042 | "dev": true, 2043 | "requires": { 2044 | "media-typer": "0.3.0", 2045 | "mime-types": "~2.1.24" 2046 | } 2047 | }, 2048 | "typescript": { 2049 | "version": "5.5.4", 2050 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", 2051 | "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", 2052 | "dev": true 2053 | }, 2054 | "unpipe": { 2055 | "version": "1.0.0", 2056 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2057 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 2058 | "dev": true 2059 | }, 2060 | "utils-merge": { 2061 | "version": "1.0.1", 2062 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 2063 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 2064 | "dev": true 2065 | }, 2066 | "vary": { 2067 | "version": "1.1.2", 2068 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2069 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 2070 | "dev": true 2071 | } 2072 | } 2073 | } 2074 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drag-drop-touch", 3 | "version": "2.0.0", 4 | "description": "A polyfill that enables HTML5 drag-and-drop support for touch devices", 5 | "type": "module", 6 | "main": "dist/drag-drop-touch.esm.js", 7 | "scripts": { 8 | "build:base": "esbuild --bundle --format=esm ./ts/drag-drop-touch.ts", 9 | "build:esm:debug": "npm run build:base -- --outfile=dist/drag-drop-touch.debug.esm.js", 10 | "build:esm": "npm run build:base -- --drop-labels=DEBUG --outfile=dist/drag-drop-touch.esm.js", 11 | "build:min": "npm run build:base -- --drop-labels=DEBUG --minify --outfile=dist/drag-drop-touch.esm.min.js", 12 | "build": "npm run lint && npm run format && npm run build:esm:debug && npm run build:esm && npm run build:min", 13 | "dev:setup": "playwright install", 14 | "format": "prettier -w ts/*.ts", 15 | "lint": "tsc --noEmit --target esnext --module esnext ts/drag-drop-touch.ts", 16 | "server": "node server.js", 17 | "start": "npm run build && npm run server", 18 | "test:integration": "playwright test tests --workers=1 --timeout 5000 --global-timeout 15000", 19 | "test": "npm run start -- -- --test && npm run test:cleanup", 20 | "test:debug": "npm run start -- -- --test --debug && npm run test:cleanup", 21 | "test:cleanup": "rm -rf test-results" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/Bernardo-Castilho/dragdroptouch.git" 26 | }, 27 | "keywords": [ 28 | "html5", 29 | "drag", 30 | "drop", 31 | "mobile", 32 | "touch" 33 | ], 34 | "author": "Bernardo Castilho (https://github.com/Bernardo-Castilho)", 35 | "contributors": [ 36 | "Andreas Rozek (https://www.rozek.de/)", 37 | "Pomax (https://pomax.github.io)" 38 | ], 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/Bernardo-Castilho/dragdroptouch/issues" 42 | }, 43 | "homepage": "https://github.com/Bernardo-Castilho/dragdroptouch#readme", 44 | "devDependencies": { 45 | "@playwright/test": "^1.45.2", 46 | "esbuild": "^0.23.0", 47 | "express": "^4.19.2", 48 | "playwright": "^1.45.2", 49 | "prettier": "^3.3.3", 50 | "typescript": "^5.5.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { readdirSync, watch } from "node:fs"; 3 | import { resolve } from "node:path"; 4 | import { spawn, spawnSync } from "node:child_process"; 5 | 6 | const PORT = process.env.PORT ?? 8000; 7 | process.env.PORT = PORT; 8 | 9 | const HOSTNAME = process.env.HOSTNAME ?? `localhost`; 10 | process.env.HOSTNAME = HOSTNAME; 11 | 12 | const testing = process.argv.includes(`--test`); 13 | const debug = process.argv.includes(`--debug`); 14 | const npm = process.platform === `win32` ? `npm.cmd` : `npm`; 15 | 16 | // Set up the core server 17 | const app = express(); 18 | app.use((req, res, next) => { 19 | res.setHeader("Surrogate-Control", "no-store"); 20 | res.setHeader( 21 | "Cache-Control", 22 | "no-store, no-cache, must-revalidate, proxy-revalidate" 23 | ); 24 | res.setHeader("Expires", "0"); 25 | next(); 26 | }); 27 | 28 | app.set("etag", false); 29 | app.use((req, res, next) => { 30 | if (!testing || (testing && debug)) { 31 | console.log(`[${new Date().toISOString()}] ${req.url}`); 32 | } 33 | next(); 34 | }); 35 | 36 | // static routes 37 | app.use(`/`, (req, res, next) => { 38 | const { url } = req; 39 | if (url === `/`) { 40 | if (testing) return res.redirect(`/tests/integration`); 41 | return res.redirect(`/demo`); 42 | } 43 | if ( 44 | url.includes(`/dist/drag-drop-touch.esm.min.js`) && 45 | (!testing || (testing && debug)) 46 | ) { 47 | return res.redirect(url.replace(`esm.min.js`, `debug.esm.js`)); 48 | } 49 | next(); 50 | }); 51 | app.use(`/`, express.static(`.`)); 52 | app.use((req, res) => { 53 | if (req.query.preview) { 54 | res.status(404).send(`Preview not found`); 55 | } else { 56 | res.status(404).send(`${req.url} not found`); 57 | } 58 | }); 59 | 60 | // Run the server, and trigger a client bundle rebuild every time script.js changes. 61 | app.listen(PORT, () => { 62 | // Generate the server address notice 63 | const msg = `= Server running on http://${HOSTNAME}:${PORT} =`; 64 | const line = `=`.repeat(msg.length); 65 | const mid = `=${` `.repeat(msg.length - 2)}=`; 66 | console.log([``, line, mid, msg, mid, line, ``].join(`\n`)); 67 | 68 | // are we running tests? 69 | if (testing) { 70 | const runner = spawn(npm, [`run`, `test:integration`], { 71 | stdio: `inherit`, 72 | }); 73 | runner.on(`close`, () => process.exit()); 74 | runner.on(`error`, () => process.exit(1)); 75 | } 76 | 77 | // we're not, run in watch mode 78 | else { 79 | try { 80 | watchForRebuild(); 81 | } catch (e) { 82 | console.error(e); 83 | } 84 | } 85 | }); 86 | 87 | /** 88 | * There's a few files we want to watch in order to rebuild the browser bundle. 89 | */ 90 | function watchForRebuild() { 91 | let rebuilding = false; 92 | 93 | async function rebuild() { 94 | if (rebuilding) return; 95 | rebuilding = true; 96 | console.log(`rebuilding`); 97 | const start = Date.now(); 98 | spawnSync(npm, [`run`, `build`], { stdio: `inherit` }); 99 | console.log(`Build took ${Date.now() - start}ms`), 8; 100 | setTimeout(() => (rebuilding = false), 500); 101 | } 102 | 103 | function watchList(list) { 104 | list.forEach((filename) => watch(resolve(filename), () => rebuild())); 105 | } 106 | 107 | watchList(readdirSync(`./ts`).map((v) => `./ts/${v}`)); 108 | } 109 | -------------------------------------------------------------------------------- /tests/integration/index.css: -------------------------------------------------------------------------------- 1 | [draggable] { 2 | user-select: none; 3 | } 4 | 5 | .dragging { 6 | opacity: 0.5; 7 | } 8 | 9 | #columns { 10 | display: flex; 11 | gap: 1rem; 12 | 13 | .column { 14 | display: inline-block; 15 | height: 150px; 16 | width: 150px; 17 | background: lightgrey; 18 | opacity: 1; 19 | 20 | header { 21 | color: #fff; 22 | padding: 5px; 23 | background: #222; 24 | pointer-events: none; 25 | } 26 | 27 | &.over { 28 | border: 2px dashed #000; 29 | opacity: 0.5; 30 | box-sizing: border-box; 31 | } 32 | 33 | & > input, 34 | & > textarea, 35 | & > select { 36 | display: block; 37 | width: 85%; 38 | margin: 5%; 39 | max-height: calc(1.25em * 4); 40 | } 41 | 42 | & > img { 43 | height: 50%; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | DragDropTouch 7 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

TEST

18 |
19 | 20 |
21 |
22 |
23 |

A box dragging example

24 |

25 | Drag some boxes around with the mouse, then open your Developer 26 | Tools, turn on mobile emulation, and try to do the same with touch 27 | input enabled. Things should still work. 28 |

29 |
30 | 31 |
32 |
33 |
Input
34 | 40 | 41 |
42 | 43 |
44 |
TextArea
45 | 46 |
47 | 48 |
49 |
Select
50 | 55 | 60 |
61 | 62 |
63 |
Image
64 | grapefruit 69 |
70 |
71 |
72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /tests/integration/index.js: -------------------------------------------------------------------------------- 1 | // Add a tiny touch simulator for CI testing 2 | import * as simulatedTouch from "./touch-simulation.js"; 3 | globalThis.simulatedTouch = simulatedTouch; 4 | 5 | let draggable = null; 6 | const cols = document.querySelectorAll(`#columns .column`); 7 | 8 | cols.forEach((col) => { 9 | col.addEventListener(`dragstart`, handleDragStart); 10 | col.addEventListener(`dragenter`, handleDragEnter); 11 | col.addEventListener(`dragover`, handleDragOver); 12 | col.addEventListener(`dragleave`, handleDragLeave); 13 | col.addEventListener(`drop`, handleDrop); 14 | col.addEventListener(`dragend`, handleDragEnd); 15 | }); 16 | 17 | function handleDragStart({ target, dataTransfer }) { 18 | if (target.className.includes(`column`)) { 19 | draggable = target; 20 | draggable.classList.add(`dragging`); 21 | 22 | dataTransfer.effectAllowed = `move`; 23 | dataTransfer.setData(`text`, draggable.innerHTML); 24 | 25 | // customize drag image for one of the panels 26 | const haveDragFn = dataTransfer.setDragImage instanceof Function; 27 | if (haveDragFn && target.textContent.includes(`X`)) { 28 | let img = new Image(); 29 | img.src = `dragimage.jpg`; 30 | dataTransfer.setDragImage(img, img.width / 2, img.height / 2); 31 | } 32 | } 33 | } 34 | 35 | function handleDragOver(evt) { 36 | if (draggable) { 37 | evt.preventDefault(); 38 | evt.dataTransfer.dropEffect = `move`; 39 | } 40 | } 41 | 42 | function handleDragEnter({ target }) { 43 | if (draggable) { 44 | target.classList.add(`over`); 45 | } 46 | } 47 | 48 | function handleDragLeave({ target }) { 49 | if (draggable) { 50 | target.classList.remove(`over`); 51 | } 52 | } 53 | 54 | function handleDragEnd() { 55 | draggable = null; 56 | cols.forEach((col) => col.classList.remove(`over`)); 57 | } 58 | 59 | function handleDrop(evt) { 60 | if (draggable === null) return; 61 | 62 | evt.stopPropagation(); 63 | evt.stopImmediatePropagation(); 64 | evt.preventDefault(); 65 | 66 | if (draggable !== this) { 67 | swapDom(draggable, this); 68 | } 69 | } 70 | 71 | // https://stackoverflow.com/questions/9732624/how-to-swap-dom-child-nodes-in-javascript 72 | function swapDom(a, b) { 73 | let aParent = a.parentNode; 74 | let bParent = b.parentNode; 75 | let aHolder = document.createElement(`div`); 76 | let bHolder = document.createElement(`div`); 77 | aParent.replaceChild(aHolder, a); 78 | bParent.replaceChild(bHolder, b); 79 | aParent.replaceChild(b, aHolder); 80 | bParent.replaceChild(a, bHolder); 81 | } 82 | -------------------------------------------------------------------------------- /tests/integration/issue-77b/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

window A

9 | 10 |

11 | 12 | 13 | 14 | 22 | testing 23 | 24 | 25 | 26 |

27 | 28 |

window B

29 | 30 |

31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/integration/issue-77b/test.js: -------------------------------------------------------------------------------- 1 | import { enableDragDropTouch } from "../../../dist/drag-drop-touch.esm.min.js"; 2 | 3 | import * as simulatedTouch from "../touch-simulation.js"; 4 | globalThis.simulatedTouch = simulatedTouch; 5 | 6 | globalThis.enablePressHold = (threshold = 0) => { 7 | enableDragDropTouch(document, document, { 8 | isPressHoldMode: true, 9 | pressHoldThresholdPixels: threshold, 10 | }); 11 | }; 12 | 13 | document.addEventListener(`dragover`, (e) => e.preventDefault()); 14 | 15 | document.addEventListener(`drop`, (e) => { 16 | e.preventDefault(); 17 | document.getElementById(`result`).textContent = 18 | e.dataTransfer.getData(`text/plain`); 19 | }); 20 | 21 | document.addEventListener(`dragstart`, (e) => 22 | e.dataTransfer.setData(`text/plain`, `we dragged a ${e.target.tagName}`) 23 | ); 24 | -------------------------------------------------------------------------------- /tests/integration/touch-simulation.js: -------------------------------------------------------------------------------- 1 | function simulate(eventType, element, { x, y }) { 2 | const touch = { 3 | clientX: x, 4 | clientY: y, 5 | }; 6 | 7 | const event = new Event(eventType, { 8 | bubbles: true, 9 | cancelable: true, 10 | target: element, 11 | ...touch, 12 | }); 13 | 14 | // We can't bind these through the Event constructor, 15 | // so we force the issue by using low level JS. 16 | Object.defineProperty(event, `touches`, { 17 | value: [touch], 18 | writable: false, 19 | }); 20 | 21 | element.dispatchEvent(event); 22 | } 23 | 24 | export /* async */ function drag(from, to, options = {}) { 25 | const { left, width, top } = from.getBoundingClientRect(); 26 | const touch = { x: left + width / 2, y: top + 1, target: from }; 27 | simulate("touchstart", from, touch); 28 | 29 | const timeout = options.dragDelay || false; 30 | 31 | // simulate a dragging track 32 | const steps = 10; 33 | const [dx, dy] = (function (l, t) { 34 | const { left, top } = to.getBoundingClientRect(); 35 | return [left - l, top - t].map((v) => v / steps); 36 | })(left, top); 37 | 38 | return new Promise((resolve) => { 39 | function drag(i = 0) { 40 | if (i === steps - 1) { 41 | simulate("touchend", to, touch); 42 | return setTimeout(resolve, 10); 43 | } 44 | touch.x += dx; 45 | touch.y += dy; 46 | touch.target = document; 47 | simulate("touchmove", to, touch); 48 | setTimeout(() => drag(i + 1), 100 / steps); 49 | } 50 | 51 | if (!timeout) drag(); 52 | else setTimeout(drag, timeout); 53 | }); 54 | } 55 | 56 | export /* async */ function tap(element) { 57 | const { left, width, top } = element.getBoundingClientRect(); 58 | const touch = { x: left + width / 2, y: top + 1, target: element }; 59 | simulate("touchstart", element, touch); 60 | 61 | return new Promise((resolve) => { 62 | setTimeout(function () { 63 | simulate("touchend", element, touch); 64 | setTimeout(resolve, 10); 65 | }, 100); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /tests/touch.spec.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | async function bootstrapPage(browser, options = {}) { 4 | const context = await browser.newContext(options); 5 | const page = await context.newPage(); 6 | page.on("console", (msg) => console.log(msg.text())); 7 | if (options.page) { 8 | await page.goto(`http://localhost:8000/${options.page}`); 9 | } else { 10 | await page.goto(`http://localhost:8000`); 11 | } 12 | return page; 13 | } 14 | 15 | test.describe(`touch events`, () => { 16 | let browser, page; 17 | 18 | test.beforeEach(async ({ browser: b }) => { 19 | browser = b; 20 | page = await bootstrapPage(browser, { 21 | hasTouch: true, 22 | }); 23 | }); 24 | 25 | async function listenForEvent(qs, eventType) { 26 | return page.evaluate((eventType) => { 27 | return new Promise((resolve) => { 28 | document.querySelector(qs).addEventListener(eventType, ({ type }) => { 29 | resolve({ type }); 30 | }); 31 | }); 32 | }, eventType); 33 | } 34 | 35 | async function touchEntry(sourceSelector) { 36 | await page.evaluate( 37 | async ({ sourceSelector }) => { 38 | const element = document.querySelector(sourceSelector); 39 | await globalThis.simulatedTouch.tap(element); 40 | }, 41 | { sourceSelector } 42 | ); 43 | } 44 | 45 | async function touchDragEntry(sourceSelector, targetSelector, options) { 46 | await page.evaluate( 47 | async ({ sourceSelector, targetSelector, options }) => { 48 | const from = document.querySelector(sourceSelector); 49 | const to = document.querySelector(targetSelector); 50 | await globalThis.simulatedTouch.drag(from, to, options); 51 | }, 52 | { sourceSelector, targetSelector, options} 53 | ); 54 | } 55 | 56 | test(`drag the left-most element onto its adjacent element`, async () => { 57 | const from = `[draggable]:nth-child(1) header`; 58 | const to = `[draggable]:nth-child(2) header`; 59 | 60 | const e1 = page.locator(from); 61 | const e2 = page.locator(to); 62 | 63 | expect(await e1.textContent()).toBe(`Input`); 64 | expect(await e2.textContent()).toBe(`TextArea`); 65 | 66 | await touchDragEntry(from, to); 67 | 68 | const e3 = page.locator(from); 69 | const e4 = page.locator(to); 70 | 71 | expect(await e3.textContent()).toBe(`TextArea`); 72 | expect(await e4.textContent()).toBe(`Input`); 73 | }); 74 | 75 | test(`drag the left-most element onto the right-most element`, async () => { 76 | const from = `[draggable]:first-child header`; 77 | const to = `[draggable]:last-child header`; 78 | 79 | const e1 = page.locator(from); 80 | const e2 = page.locator(to); 81 | 82 | expect(await e1.textContent()).toBe(`Input`); 83 | expect(await e2.textContent()).toBe(`Image`); 84 | 85 | await touchDragEntry(from, to); 86 | 87 | const e3 = page.locator(from); 88 | const e4 = page.locator(to); 89 | 90 | expect(await e3.textContent()).toBe(`Image`); 91 | expect(await e4.textContent()).toBe(`Input`); 92 | }); 93 | 94 | test(`drag the right-most element by touch-dragging the image`, async () => { 95 | const from = `[draggable]:last-child img`; 96 | const to = `[draggable]:first-child header`; 97 | await touchDragEntry(from, to); 98 | 99 | const e1 = page.locator(from.replace(`img`, `header`)); 100 | const e2 = page.locator(to); 101 | 102 | expect(await e1.textContent()).toBe(`Input`); 103 | expect(await e2.textContent()).toBe(`Image`); 104 | }); 105 | 106 | test(`longpress with 0px drag threshold`, async () => { 107 | page = await bootstrapPage(browser, { 108 | hasTouch: true, 109 | page: `tests/integration/issue-77b/index.html`, 110 | }); 111 | 112 | const textContent = await page.locator(`text`).textContent(); 113 | expect(textContent.trim()).toBe(`testing`); 114 | 115 | await page.evaluate(() => globalThis.enablePressHold()); 116 | const from = `#from`; 117 | const to = `#to`; 118 | await touchDragEntry(from, to, { dragDelay: 500 }); 119 | 120 | expect(await page.locator(`#result`).textContent()).toBe(`we dragged a A`); 121 | }); 122 | 123 | test(`longpress with 25px drag threshold`, async () => { 124 | page = await bootstrapPage(browser, { 125 | hasTouch: true, 126 | page: `tests/integration/issue-77b/index.html`, 127 | }); 128 | 129 | const textContent = await page.locator(`text`).textContent(); 130 | expect(textContent.trim()).toBe(`testing`); 131 | 132 | await page.evaluate(() => globalThis.enablePressHold(25)); 133 | const from = `#from`; 134 | const to = `#to`; 135 | await touchDragEntry(from, to, { dragDelay: 500 }); 136 | 137 | expect(await page.locator(`#result`).textContent()).toBe(`we dragged a A`); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /ts/drag-drop-touch-util.ts: -------------------------------------------------------------------------------- 1 | type Mutable = { 2 | -readonly [P in keyof T | K]: T[P]; 3 | }; 4 | 5 | /** 6 | * ...docs go here... 7 | * @param e 8 | * @param page 9 | * @returns 10 | */ 11 | export function pointFrom(e: TouchEvent, page = false) { 12 | const touch = e.touches[0]; 13 | return { 14 | x: page ? touch.pageX : touch.clientX, 15 | y: page ? touch.pageY : touch.clientY, 16 | }; 17 | } 18 | 19 | /** 20 | * ...docs go here... 21 | * @param dst 22 | * @param src 23 | * @param props 24 | */ 25 | export function copyProps( 26 | dst: Record, 27 | src: Record, 28 | props: Array, 29 | ) { 30 | for (let i = 0; i < props.length; i++) { 31 | let p = props[i]; 32 | dst[p] = src[p]; 33 | } 34 | } 35 | 36 | /** 37 | * ...docs go here... 38 | * @param type 39 | * @param srcEvent 40 | * @param target 41 | * @returns 42 | */ 43 | export function newForwardableEvent( 44 | type: keyof GlobalEventHandlersEventMap, 45 | srcEvent: TouchEvent, 46 | target: HTMLElement, 47 | ) { 48 | const _kbdProps = ["altKey", "ctrlKey", "metaKey", "shiftKey"]; 49 | const _ptProps = [ 50 | "pageX", 51 | "pageY", 52 | "clientX", 53 | "clientY", 54 | "screenX", 55 | "screenY", 56 | "offsetX", 57 | "offsetY", 58 | ]; 59 | const evt = new Event(type, { 60 | bubbles: true, 61 | cancelable: true, 62 | }) as unknown as Mutable & { 63 | readonly defaultPrevented: boolean; 64 | }, 65 | touch = srcEvent.touches[0]; 66 | evt.button = 0; 67 | evt.which = evt.buttons = 1; 68 | copyProps(evt, srcEvent, _kbdProps); 69 | copyProps(evt, touch, _ptProps); 70 | setOffsetAndLayerProps(evt, target); 71 | return evt; 72 | } 73 | 74 | /** 75 | * ...docs go here... 76 | * @param e 77 | * @param target 78 | */ 79 | function setOffsetAndLayerProps( 80 | e: Mutable< 81 | MouseEvent, 82 | `${"client" | "layer" | "offset" | "page"}${"X" | "Y"}` 83 | >, 84 | target: HTMLElement, 85 | ) { 86 | const rect = target.getBoundingClientRect(); 87 | if (e.offsetX === undefined) { 88 | e.offsetX = e.clientX - rect.x; 89 | e.offsetY = e.clientY - rect.y; 90 | } 91 | if (e.layerX === undefined) { 92 | e.layerX = e.pageX - rect.left; 93 | e.layerY = e.pageY - rect.top; 94 | } 95 | } 96 | 97 | /** 98 | * ...docs go here... 99 | * @param src 100 | * @param dst 101 | */ 102 | export function copyStyle(src: HTMLElement, dst: HTMLElement) { 103 | // remove potentially troublesome attributes 104 | removeTroublesomeAttributes(dst); 105 | 106 | // copy canvas content 107 | if (src instanceof HTMLCanvasElement) { 108 | let cDst = dst as HTMLCanvasElement; 109 | cDst.width = src.width; 110 | cDst.height = src.height; 111 | cDst.getContext("2d")!.drawImage(src, 0, 0); 112 | } 113 | 114 | // copy style (without transitions) 115 | copyComputedStyles(src, dst); 116 | dst.style.pointerEvents = "none"; 117 | 118 | // and repeat for all children 119 | for (let i = 0; i < src.children.length; i++) { 120 | copyStyle(src.children[i] as HTMLElement, dst.children[i] as HTMLElement); 121 | } 122 | } 123 | 124 | /** 125 | * ...docs go here... 126 | * @param src 127 | * @param dst 128 | * @param copyKey 129 | */ 130 | function copyComputedStyles(src: any, dst: any) { 131 | let cs = getComputedStyle(src); 132 | for (let key of cs) { 133 | if (key.includes("transition")) continue; 134 | dst.style[key] = cs[key]; 135 | } 136 | Object.keys(dst.dataset).forEach((key) => delete dst.dataset[key]); 137 | } 138 | 139 | /** 140 | * ...docs go here... 141 | * @param dst 142 | */ 143 | function removeTroublesomeAttributes(dst: any) { 144 | ["id", "class", "style", "draggable"].forEach(function (att) { 145 | dst.removeAttribute(att); 146 | }); 147 | } 148 | -------------------------------------------------------------------------------- /ts/drag-drop-touch.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { 4 | copyStyle, 5 | newForwardableEvent, 6 | pointFrom, 7 | } from "./drag-drop-touch-util"; 8 | import { DragDTO } from "./drag-dto"; 9 | 10 | const { round } = Math; 11 | 12 | type DragDropTouchConfiguration = { 13 | // A flag that determines whether to allow scrolling 14 | // when a drag reaches the edges of the screen. 15 | allowDragScroll: boolean; 16 | 17 | // The number of milliseconds we'll wait before we 18 | // trigger a context menu event on long press. 19 | contextMenuDelayMS: number; 20 | 21 | // How see-through should the "drag placeholder", 22 | // that's attached to the cursor while dragging, be? 23 | dragImageOpacity: number; 24 | 25 | // The size of the "hot region" at the edge of the 26 | // screen on which scrolling will be allowed, if 27 | // the allowDragScroll flag is true (the default). 28 | dragScrollPercentage: number; 29 | 30 | // The number of pixels to scroll if a drag event 31 | // occurs within a scrolling hot region. 32 | dragScrollSpeed: number; 33 | 34 | // How much do we need to touchmove before the code 35 | // switches to drag mode rather than click mode? 36 | dragThresholdPixels: number; 37 | 38 | // The flag that tells us whether a long-press should 39 | // count as a user signal to "pick up an item" for 40 | // drag and drop purposes. 41 | isPressHoldMode: boolean; 42 | 43 | // A flag that determines whether the code should ignore 44 | // the navigator.maxTouchPoints value, which normally 45 | // tells us whether to expect touch events or not. 46 | forceListen: boolean; 47 | 48 | // The number of milliseconds we'll wait before we 49 | // consider an active press to be a "long press". 50 | pressHoldDelayMS: number; 51 | 52 | // The number of pixels we allow a touch event to 53 | // drift over the course of a long press start. 54 | pressHoldMargin: number; 55 | 56 | // The drift in pixels that determines whether a 57 | // long press starts a long press, or a touch-drag. 58 | pressHoldThresholdPixels: number; 59 | }; 60 | 61 | const DefaultConfiguration: DragDropTouchConfiguration = { 62 | allowDragScroll: true, 63 | contextMenuDelayMS: 900, 64 | dragImageOpacity: 0.5, 65 | dragScrollPercentage: 10, 66 | dragScrollSpeed: 10, 67 | dragThresholdPixels: 5, 68 | forceListen: false, 69 | isPressHoldMode: false, 70 | pressHoldDelayMS: 400, 71 | pressHoldMargin: 25, 72 | pressHoldThresholdPixels: 0, 73 | }; 74 | 75 | interface Point { 76 | x: number; 77 | y: number; 78 | } 79 | 80 | /** 81 | * Defines a class that adds support for touch-based HTML5 drag/drop operations. 82 | * 83 | * The @see:DragDropTouch class listens to touch events and raises the 84 | * appropriate HTML5 drag/drop events as if the events had been caused 85 | * by mouse actions. 86 | * 87 | * The purpose of this class is to enable using existing, standard HTML5 88 | * drag/drop code on mobile devices running IOS or Android. 89 | * 90 | * To use, include the DragDropTouch.js file on the page. The class will 91 | * automatically start monitoring touch events and will raise the HTML5 92 | * drag drop events (`dragstart`, `dragenter`, `dragleave`, `drop`, `dragend`) which 93 | * should be handled by the application. 94 | * 95 | * For details and examples on HTML drag and drop, see 96 | * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations. 97 | */ 98 | class DragDropTouch { 99 | private readonly _dragRoot: Document | Element; 100 | private _dropRoot: Document | ShadowRoot; 101 | private _dragSource: EventTarget | null; 102 | private _lastTouch: TouchEvent | null; 103 | private _lastTarget: EventTarget | null; 104 | private _ptDown: Point | null; 105 | private _isDragEnabled: boolean; 106 | private _isDropZone: boolean; 107 | private _dataTransfer: DragDTO; 108 | private _img: HTMLElement | null; 109 | private _imgCustom: HTMLElement | null; 110 | private _imgOffset: Point; 111 | private _pressHoldIntervalId?: ReturnType | undefined; 112 | 113 | private readonly configuration: DragDropTouchConfiguration; 114 | 115 | /** 116 | * Deal with shadow DOM elements. 117 | * 118 | * Previous implementation used `document.elementFromPoint` to find the dropped upon 119 | * element. This, however, doesn't "pierce" the shadow DOM. So instead, we can 120 | * provide a drop tree element to search within. It would be nice if `elementFromPoint` 121 | * were implemented on this node (arbitrarily), but it only appears on documents and 122 | * shadow roots. So here we simply walk up the DOM tree until we find that method. 123 | * 124 | * In fact this does NOT restrict dropping to just the root provided-- but the whole 125 | * tree. I'm not sure that this is a general solution, but works for my specific and 126 | * the general one. 127 | * 128 | * @param dragRoot 129 | * @param options 130 | */ 131 | constructor( 132 | dragRoot: Document | Element = document, 133 | dropRoot: Document | Element = document, 134 | options?: Partial, 135 | ) { 136 | this.configuration = { ...DefaultConfiguration, ...(options || {}) }; 137 | this._dragRoot = dragRoot; 138 | this._dropRoot = dropRoot as any; 139 | while ( 140 | !(this._dropRoot as any).elementFromPoint && 141 | this._dropRoot.parentNode 142 | ) 143 | this._dropRoot = this._dropRoot.parentNode as any; 144 | this._dragSource = null; 145 | this._lastTouch = null; 146 | this._lastTarget = null; 147 | this._ptDown = null; 148 | this._isDragEnabled = false; 149 | this._isDropZone = false; 150 | this._dataTransfer = new DragDTO(this); 151 | this._img = null; 152 | this._imgCustom = null; 153 | this._imgOffset = { x: 0, y: 0 }; 154 | this.listen(); 155 | } 156 | 157 | /** 158 | * ...docs go here... 159 | * @returns 160 | */ 161 | listen() { 162 | if (navigator.maxTouchPoints === 0 && !this.configuration.forceListen) { 163 | return; 164 | } 165 | 166 | const opt = { passive: false, capture: false }; 167 | 168 | this._dragRoot.addEventListener( 169 | `touchstart`, 170 | this._touchstart.bind(this) as EventListener, 171 | opt, 172 | ); 173 | this._dragRoot.addEventListener( 174 | `touchmove`, 175 | this._touchmove.bind(this) as EventListener, 176 | opt, 177 | ); 178 | this._dragRoot.addEventListener( 179 | `touchend`, 180 | this._touchend.bind(this) as EventListener, 181 | ); 182 | this._dragRoot.addEventListener( 183 | `touchcancel`, 184 | this._touchend.bind(this) as EventListener, 185 | ); 186 | } 187 | 188 | /** 189 | * ...docs go here... 190 | * @param img 191 | * @param offsetX 192 | * @param offsetY 193 | */ 194 | setDragImage(img: HTMLElement, offsetX: number, offsetY: number) { 195 | this._imgCustom = img; 196 | this._imgOffset = { x: offsetX, y: offsetY }; 197 | } 198 | 199 | /** 200 | * ...docs go here... 201 | * @param e 202 | */ 203 | _touchstart(e: TouchEvent) { 204 | DEBUG: console.log(`touchstart`); 205 | if (this._shouldHandle(e)) { 206 | DEBUG: console.log(`handling touch start, resetting state`); 207 | this._reset(); 208 | let src = this._closestDraggable(e.target as HTMLElement); 209 | if (src) { 210 | // give caller a chance to handle the hover/move events 211 | if ( 212 | e.target && 213 | !this._dispatchEvent(e, `mousemove`, e.target) && 214 | !this._dispatchEvent(e, `mousedown`, e.target!) 215 | ) { 216 | // get ready to start dragging 217 | this._dragSource = src; 218 | this._ptDown = pointFrom(e); 219 | this._lastTouch = e; 220 | 221 | // show context menu if the user hasn't started dragging after a while 222 | DEBUG: console.log(`setting a contextmenu timeout`); 223 | setTimeout(() => { 224 | if (this._dragSource === src && this._img === null) { 225 | if (this._dispatchEvent(e, `contextmenu`, src)) { 226 | this._reset(); 227 | } 228 | } 229 | }, this.configuration.contextMenuDelayMS); 230 | 231 | if (this.configuration.isPressHoldMode) { 232 | DEBUG: console.log( 233 | `setting a press-hold timeout for ${this.configuration.pressHoldDelayMS}ms`, 234 | ); 235 | this._pressHoldIntervalId = setTimeout(() => { 236 | DEBUG: console.log( 237 | `this._isDragEnabled = true, calling touchMove`, 238 | ); 239 | this._isDragEnabled = true; 240 | this._touchmove(e); 241 | }, this.configuration.pressHoldDelayMS); 242 | } 243 | 244 | // We need this in case we're dealing with simulated touch events, 245 | // in which case the touch start + touch end won't have automagically 246 | // been turned into click events by the browser. 247 | else if (!e.isTrusted) { 248 | if (e.target !== this._lastTarget) { 249 | DEBUG: console.log(`synthetic touch start: saving _lastTarget`); 250 | this._lastTarget = e.target; 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } 257 | 258 | /** 259 | * ...docs go here... 260 | * @param e 261 | * @returns 262 | */ 263 | _touchmove(e: TouchEvent) { 264 | DEBUG: console.log(`touchmove`); 265 | 266 | if (this._shouldCancelPressHoldMove(e)) { 267 | DEBUG: console.log(`cancel press-hold move`); 268 | this._reset(); 269 | return; 270 | } 271 | 272 | if (this._shouldHandleMove(e) || this._shouldHandlePressHoldMove(e)) { 273 | DEBUG: console.log(`handling touch move`); 274 | 275 | // see if target wants to handle move 276 | let target = this._getTarget(e)!; 277 | if (this._dispatchEvent(e, `mousemove`, target)) { 278 | DEBUG: console.log(`target handled mousemove, returning early.`); 279 | this._lastTouch = e; 280 | e.preventDefault(); 281 | return; 282 | } 283 | 284 | // start dragging 285 | if (this._dragSource && !this._img && this._shouldStartDragging(e)) { 286 | DEBUG: console.log(`should start dragging`); 287 | if ( 288 | this._dispatchEvent(this._lastTouch!, `dragstart`, this._dragSource) 289 | ) { 290 | DEBUG: console.log(`target canceled drag event, returning early.`); 291 | this._dragSource = null; 292 | return; 293 | } 294 | this._createImage(e); 295 | this._dispatchEvent(e, `dragenter`, target); 296 | } 297 | 298 | // continue dragging 299 | if (this._img && this._dragSource) { 300 | DEBUG: console.log(`continue dragging`); 301 | this._lastTouch = e; 302 | e.preventDefault(); 303 | 304 | this._dispatchEvent(e, `drag`, this._dragSource); 305 | if (target !== this._lastTarget) { 306 | if (this._lastTarget) 307 | this._dispatchEvent(this._lastTouch, `dragleave`, this._lastTarget); 308 | this._dispatchEvent(e, `dragenter`, target); 309 | this._lastTarget = target; 310 | } 311 | this._moveImage(e); 312 | this._isDropZone = this._dispatchEvent(e, `dragover`, target); 313 | 314 | // Allow scrolling if the screen edges were marked as "hot regions". 315 | if (this.configuration.allowDragScroll) { 316 | DEBUG: console.log(`synthetic scroll allowed`); 317 | const delta = this._getHotRegionDelta(e); 318 | globalThis.scrollBy(delta.x, delta.y); 319 | } 320 | } 321 | } 322 | } 323 | 324 | /** 325 | * ...docs go here... 326 | * @param e 327 | * @returns 328 | */ 329 | _touchend(e: TouchEvent) { 330 | DEBUG: console.log(`touchend`); 331 | 332 | if (!(this._lastTouch && e.target && this._lastTarget)) { 333 | DEBUG: console.log(`no lastTouch for touchend, resetting state.`); 334 | this._reset(); 335 | return; 336 | } 337 | 338 | if (this._shouldHandle(e)) { 339 | DEBUG: console.log(`handling touch end`); 340 | 341 | if (this._dispatchEvent(this._lastTouch, `mouseup`, e.target)) { 342 | DEBUG: console.log(`target handled mouseup, returning early.`); 343 | e.preventDefault(); 344 | return; 345 | } 346 | 347 | // user clicked the element but didn't drag, so clear the source and simulate a click 348 | if (!this._img) { 349 | DEBUG: console.log(`click rather than drag.`); 350 | this._dragSource = null; 351 | this._dispatchEvent(this._lastTouch, `click`, e.target); 352 | } 353 | 354 | // finish dragging 355 | this._destroyImage(); 356 | if (this._dragSource) { 357 | DEBUG: console.log(`handling drop.`); 358 | if (e.type.indexOf(`cancel`) < 0 && this._isDropZone) { 359 | DEBUG: console.log(`drop was canceled.`); 360 | this._dispatchEvent(this._lastTouch, `drop`, this._lastTarget); 361 | } 362 | this._dispatchEvent(this._lastTouch, `dragend`, this._dragSource); 363 | this._reset(); 364 | } 365 | } 366 | } 367 | 368 | /** 369 | * ...docs go here... 370 | * @param e 371 | * @returns 372 | */ 373 | _shouldHandle(e: TouchEvent) { 374 | return e && !e.defaultPrevented && e.touches && e.touches.length < 2; 375 | } 376 | 377 | /** 378 | * ...docs go here... 379 | * @param e 380 | * @returns 381 | */ 382 | _shouldHandleMove(e: TouchEvent) { 383 | return !this.configuration.isPressHoldMode && this._shouldHandle(e); 384 | } 385 | 386 | /** 387 | * ...docs go here... 388 | * @param e 389 | * @returns 390 | */ 391 | _shouldHandlePressHoldMove(e: TouchEvent) { 392 | return ( 393 | this.configuration.isPressHoldMode && 394 | this._isDragEnabled && 395 | e && 396 | e.touches && 397 | e.touches.length 398 | ); 399 | } 400 | 401 | /** 402 | * ...docs go here... 403 | * @param e 404 | * @returns 405 | */ 406 | _shouldCancelPressHoldMove(e: TouchEvent) { 407 | DEBUG: { 408 | console.log({ 409 | isPressHoldMode: this.configuration.isPressHoldMode, 410 | _isDragEnabled: this._isDragEnabled, 411 | delta: this._getDelta(e), 412 | pressHoldMargin: this.configuration.pressHoldMargin, 413 | }); 414 | } 415 | return ( 416 | this.configuration.isPressHoldMode && 417 | !this._isDragEnabled && 418 | this._getDelta(e) > this.configuration.pressHoldMargin 419 | ); 420 | } 421 | 422 | /** 423 | * ...docs go here... 424 | * @param e 425 | * @returns 426 | */ 427 | _shouldStartDragging(e: TouchEvent) { 428 | let delta = this._getDelta(e); 429 | if (this.configuration.isPressHoldMode) { 430 | DEBUG: console.log( 431 | this.configuration.isPressHoldMode, 432 | delta, 433 | this.configuration.pressHoldThresholdPixels, 434 | ); 435 | return delta >= this.configuration.pressHoldThresholdPixels; 436 | } 437 | return delta > this.configuration.dragThresholdPixels; 438 | } 439 | 440 | /** 441 | * ...docs go here... 442 | */ 443 | _reset() { 444 | this._destroyImage(); 445 | this._dragSource = null; 446 | this._lastTouch = null; 447 | this._lastTarget = null; 448 | this._ptDown = null; 449 | this._isDragEnabled = false; 450 | this._isDropZone = false; 451 | this._dataTransfer = new DragDTO(this); 452 | clearTimeout(this._pressHoldIntervalId); 453 | } 454 | 455 | /** 456 | * ...docs go here... 457 | * @param e 458 | * @returns 459 | */ 460 | _getDelta(e: TouchEvent) { 461 | // if there is no active touch we don't need to calculate anything. 462 | if (!this._ptDown) return 0; 463 | 464 | // Determine how `far` from the event coordinate our 465 | // original touch coordinate was. 466 | const { x, y } = this._ptDown; 467 | const p = pointFrom(e); 468 | return ((p.x - x) ** 2 + (p.y - y) ** 2) ** 0.5; 469 | } 470 | 471 | /** 472 | * ...docs go here... 473 | * @param e 474 | */ 475 | _getHotRegionDelta(e: TouchEvent) { 476 | const { clientX: x, clientY: y } = e.touches[0]; 477 | const { innerWidth: w, innerHeight: h } = globalThis; 478 | const { dragScrollPercentage, dragScrollSpeed } = this.configuration; 479 | const v1 = dragScrollPercentage / 100; 480 | const v2 = 1 - v1; 481 | const dx = 482 | x < w * v1 ? -dragScrollSpeed : x > w * v2 ? +dragScrollSpeed : 0; 483 | const dy = 484 | y < h * v1 ? -dragScrollSpeed : y > h * v2 ? +dragScrollSpeed : 0; 485 | return { x: dx, y: dy }; 486 | } 487 | 488 | /** 489 | * ...docs go here... 490 | * @param e 491 | * @returns 492 | */ 493 | _getTarget(e: TouchEvent) { 494 | let pt = pointFrom(e), 495 | el = this._dropRoot.elementFromPoint(pt.x, pt.y); 496 | while (el && getComputedStyle(el).pointerEvents == `none`) { 497 | el = el.parentElement; 498 | } 499 | return el; 500 | } 501 | 502 | /** 503 | * ...docs go here... 504 | * @param e 505 | */ 506 | _createImage(e: TouchEvent) { 507 | // just in case... 508 | if (this._img) { 509 | this._destroyImage(); 510 | } 511 | // create drag image from custom element or drag source 512 | let src = this._imgCustom || (this._dragSource as HTMLElement); 513 | this._img = src.cloneNode(true) as HTMLElement; 514 | copyStyle(src, this._img); 515 | this._img.style.top = this._img.style.left = `-9999px`; 516 | // if creating from drag source, apply offset and opacity 517 | if (!this._imgCustom) { 518 | let rc = src.getBoundingClientRect(), 519 | pt = pointFrom(e); 520 | this._imgOffset = { x: pt.x - rc.left, y: pt.y - rc.top }; 521 | this._img.style.opacity = `${this.configuration.dragImageOpacity}`; 522 | } 523 | // add image to document 524 | this._moveImage(e); 525 | document.body.appendChild(this._img); 526 | } 527 | 528 | /** 529 | * ...docs go here... 530 | */ 531 | _destroyImage() { 532 | if (this._img && this._img.parentElement) { 533 | this._img.parentElement.removeChild(this._img); 534 | } 535 | this._img = null; 536 | this._imgCustom = null; 537 | } 538 | 539 | /** 540 | * ...docs go here... 541 | * @param e 542 | */ 543 | _moveImage(e: TouchEvent) { 544 | requestAnimationFrame(() => { 545 | if (this._img) { 546 | let pt = pointFrom(e, true), 547 | s = this._img.style; 548 | s.position = `absolute`; 549 | s.pointerEvents = `none`; 550 | s.zIndex = `999999`; 551 | s.left = `${round(pt.x - this._imgOffset.x)}px`; 552 | s.top = `${round(pt.y - this._imgOffset.y)}px`; 553 | } 554 | }); 555 | } 556 | 557 | /** 558 | * ...docs go here... 559 | * @param srcEvent 560 | * @param type 561 | * @param target 562 | * @returns 563 | */ 564 | _dispatchEvent( 565 | srcEvent: TouchEvent, 566 | type: keyof GlobalEventHandlersEventMap, 567 | target: EventTarget, 568 | ) { 569 | if (!(srcEvent && target)) return false; 570 | const evt = newForwardableEvent(type, srcEvent, target as HTMLElement); 571 | 572 | // DragEvents need a data transfer object 573 | (evt as any).dataTransfer = this._dataTransfer; 574 | target.dispatchEvent(evt as unknown as Event); 575 | return evt.defaultPrevented; 576 | } 577 | 578 | /** 579 | * ...docs go here... 580 | * @param el 581 | * @returns 582 | */ 583 | _closestDraggable(element: HTMLElement | null) { 584 | for (let e = element; e !== null; e = e.parentElement) { 585 | if (e.draggable) { 586 | return e; 587 | } 588 | } 589 | return null; 590 | } 591 | } 592 | 593 | /** 594 | * Offer users a setup function rather than the class itself 595 | * 596 | * @param dragRoot 597 | * @param options 598 | */ 599 | export function enableDragDropTouch( 600 | dragRoot: Document | Element = document, 601 | dropRoot: Document | Element = document, 602 | options?: Partial, 603 | ) { 604 | new DragDropTouch(dragRoot, dropRoot, options); 605 | } 606 | 607 | // Take advantage of ESM's ability to know which URL it's being 608 | // loaded from, by automatically building the singleton class 609 | // instance if we're being loaded with ?autoload as part of the 610 | // import URL. 611 | if (import.meta.url.includes(`?autoload`)) { 612 | enableDragDropTouch(document, document, { 613 | forceListen: true, 614 | }); 615 | } 616 | 617 | // If we're not autoloading, expose DragDropTouch but not as the 618 | // class itself but as an object with an .enable() function. 619 | else { 620 | globalThis.DragDropTouch = { 621 | enable: function ( 622 | dragRoot: Document | Element = document, 623 | dropRoot: Document | Element = document, 624 | options?: Partial, 625 | ): void { 626 | enableDragDropTouch(dragRoot, dropRoot, options); 627 | }, 628 | }; 629 | } 630 | -------------------------------------------------------------------------------- /ts/drag-dto.ts: -------------------------------------------------------------------------------- 1 | type DDT = { 2 | setDragImage: (img: HTMLElement, offsetX: number, offsetY: number) => void; 3 | }; 4 | 5 | /** 6 | * Object used to hold the data that is being dragged during drag and drop operations. 7 | * 8 | * It may hold one or more data items of different types. For more information about 9 | * drag and drop operations and data transfer objects, see 10 | * HTML Drag and Drop API. 11 | * 12 | * This object is created automatically by the @see:DragDropTouch and is 13 | * accessible through the @see:dataTransfer property of all drag events. 14 | */ 15 | export class DragDTO { 16 | protected _dropEffect: any; 17 | private _effectAllowed: any; 18 | private _data: any; 19 | private _dragDropTouch: any; 20 | 21 | constructor(dragDropTouch: DDT) { 22 | this._dropEffect = "move"; 23 | this._effectAllowed = "all"; 24 | this._data = {}; 25 | this._dragDropTouch = dragDropTouch; 26 | } 27 | 28 | get dropEffect() { 29 | return this._dropEffect; 30 | } 31 | 32 | set dropEffect(value) { 33 | this._dropEffect = value; 34 | } 35 | 36 | get effectAllowed() { 37 | return this._effectAllowed; 38 | } 39 | 40 | set effectAllowed(value) { 41 | this._effectAllowed = value; 42 | } 43 | 44 | get types() { 45 | return Object.keys(this._data); 46 | } 47 | 48 | /** 49 | * ...docs go here... 50 | * @param type 51 | */ 52 | clearData(type: string) { 53 | if (type !== null) { 54 | delete this._data[type.toLowerCase()]; 55 | } else { 56 | this._data = {}; 57 | } 58 | } 59 | 60 | /** 61 | * ...docs go here... 62 | * @param type 63 | * @returns 64 | */ 65 | getData(type: string) { 66 | let lcType = type.toLowerCase(), 67 | data = this._data[lcType]; 68 | if (lcType === "text" && data == null) { 69 | data = this._data["text/plain"]; // getData("text") also gets ("text/plain") 70 | } 71 | return data; // @see https://github.com/Bernardo-Castilho/dragdroptouch/pull/61/files 72 | } 73 | 74 | /** 75 | * ...docs go here... 76 | * @param type 77 | * @param value 78 | */ 79 | setData(type: string, value: any) { 80 | this._data[type.toLowerCase()] = value; 81 | } 82 | 83 | /** 84 | * ...docs go here... 85 | * @param img 86 | * @param offsetX 87 | * @param offsetY 88 | */ 89 | setDragImage(img: Element, offsetX: number, offsetY: number) { 90 | this._dragDropTouch.setDragImage(img, offsetX, offsetY); 91 | } 92 | } 93 | --------------------------------------------------------------------------------