├── .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 |
41 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | one
59 | two
60 | three
61 |
62 |
63 | one
64 | two
65 | three
66 |
67 |
68 |
69 |
70 |
71 |
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 |
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 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | one
52 | two
53 | three
54 |
55 |
56 | one
57 | two
58 | three
59 |
60 |
61 |
62 |
63 |
64 |
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 |
--------------------------------------------------------------------------------