@k2b/ui 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/browser/actions/ContextMenu.js +5 -0
  2. package/dist/browser/actions/ContextMenu.js.map +1 -1
  3. package/dist/browser/actions/DragAndDrop.js +89 -0
  4. package/dist/browser/actions/DragAndDrop.js.map +1 -0
  5. package/dist/browser/actions/GestureMenu.js +26 -3
  6. package/dist/browser/actions/GestureMenu.js.map +1 -1
  7. package/dist/browser/actions/drag-and-drop-messages.js +75 -0
  8. package/dist/browser/actions/drag-and-drop-messages.js.map +1 -0
  9. package/dist/browser/actions/drag-and-drop.js +692 -0
  10. package/dist/browser/actions/drag-and-drop.js.map +1 -0
  11. package/dist/browser/actions/index.js +1 -0
  12. package/dist/browser/actions/index.js.map +1 -1
  13. package/dist/browser/chat/ChatComposer.js +385 -265
  14. package/dist/browser/chat/ChatComposer.js.map +1 -1
  15. package/dist/browser/chat/ChatDecision.js +778 -0
  16. package/dist/browser/chat/ChatDecision.js.map +1 -0
  17. package/dist/browser/chat/ChatPrimitives.js +34 -28
  18. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  19. package/dist/browser/chat/ChatTimeline.js.map +1 -1
  20. package/dist/browser/chat/chat-messages.js +6 -0
  21. package/dist/browser/chat/chat-messages.js.map +1 -1
  22. package/dist/browser/chat/command-search.js +10 -3
  23. package/dist/browser/chat/command-search.js.map +1 -1
  24. package/dist/browser/chat/index.js +6 -1
  25. package/dist/browser/chat/index.js.map +1 -1
  26. package/dist/browser/chat/types.js.map +1 -1
  27. package/dist/browser/content/Calendar.js +3 -3
  28. package/dist/browser/content/Calendar.js.map +1 -1
  29. package/dist/browser/content/Chart.js +54 -71
  30. package/dist/browser/content/Chart.js.map +1 -1
  31. package/dist/browser/content/FileBrowser.js +2 -2
  32. package/dist/browser/content/FileBrowser.js.map +1 -1
  33. package/dist/browser/content/FileTree.js +42 -36
  34. package/dist/browser/content/FileTree.js.map +1 -1
  35. package/dist/browser/content/FileTypeIcon.js +72 -0
  36. package/dist/browser/content/FileTypeIcon.js.map +1 -0
  37. package/dist/browser/content/FileView.js +2 -2
  38. package/dist/browser/content/FileView.js.map +1 -1
  39. package/dist/browser/content/ReferenceChip.js +162 -0
  40. package/dist/browser/content/ReferenceChip.js.map +1 -0
  41. package/dist/browser/content/calendar-pointer.js +9 -0
  42. package/dist/browser/content/calendar-pointer.js.map +1 -1
  43. package/dist/browser/content/calendar-time-view.js +6 -3
  44. package/dist/browser/content/calendar-time-view.js.map +1 -1
  45. package/dist/browser/content/index.js +2 -0
  46. package/dist/browser/content/index.js.map +1 -1
  47. package/dist/browser/layout/AppOverview.js +1 -1
  48. package/dist/browser/layout/AppOverview.js.map +1 -1
  49. package/dist/browser/layout/AppWorkspace.js +644 -445
  50. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  51. package/dist/browser/layout/PanelDialog.js +88 -86
  52. package/dist/browser/layout/PanelDialog.js.map +1 -1
  53. package/dist/browser/layout/ResizeSeparator.js +81 -0
  54. package/dist/browser/layout/ResizeSeparator.js.map +1 -0
  55. package/dist/browser/layout/SidebarSwitcher.js +9 -2
  56. package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
  57. package/dist/browser/layout/TabBar.js +15 -12
  58. package/dist/browser/layout/TabBar.js.map +1 -1
  59. package/dist/browser/layout/app-workspace-controller.js +130 -17
  60. package/dist/browser/layout/app-workspace-controller.js.map +1 -1
  61. package/dist/browser/layout/index.js +3 -1
  62. package/dist/browser/layout/index.js.map +1 -1
  63. package/dist/browser/layout/resize-separator.js +90 -0
  64. package/dist/browser/layout/resize-separator.js.map +1 -0
  65. package/dist/browser/layout/workspace-messages.js +12 -2
  66. package/dist/browser/layout/workspace-messages.js.map +1 -1
  67. package/dist/browser/surfaces/LinkCard.js +1 -1
  68. package/dist/browser/surfaces/LinkCard.js.map +1 -1
  69. package/dist/browser/surfaces/Placeholder.js +14 -3
  70. package/dist/browser/surfaces/Placeholder.js.map +1 -1
  71. package/dist/browser/surfaces/StatCell.js +23 -0
  72. package/dist/browser/surfaces/StatCell.js.map +1 -1
  73. package/dist/browser/widgets/Widget.js +1 -1
  74. package/dist/browser/widgets/Widget.js.map +1 -1
  75. package/dist/ssr/actions/ContextMenu.js +5 -0
  76. package/dist/ssr/actions/ContextMenu.js.map +1 -1
  77. package/dist/ssr/actions/DragAndDrop.js +63 -0
  78. package/dist/ssr/actions/DragAndDrop.js.map +1 -0
  79. package/dist/ssr/actions/GestureMenu.js +26 -3
  80. package/dist/ssr/actions/GestureMenu.js.map +1 -1
  81. package/dist/ssr/actions/drag-and-drop-messages.js +75 -0
  82. package/dist/ssr/actions/drag-and-drop-messages.js.map +1 -0
  83. package/dist/ssr/actions/drag-and-drop.js +692 -0
  84. package/dist/ssr/actions/drag-and-drop.js.map +1 -0
  85. package/dist/ssr/actions/index.js +1 -0
  86. package/dist/ssr/actions/index.js.map +1 -1
  87. package/dist/ssr/chat/ChatComposer.js +131 -14
  88. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  89. package/dist/ssr/chat/ChatDecision.js +465 -0
  90. package/dist/ssr/chat/ChatDecision.js.map +1 -0
  91. package/dist/ssr/chat/ChatPrimitives.js +2 -2
  92. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  93. package/dist/ssr/chat/ChatTimeline.js.map +1 -1
  94. package/dist/ssr/chat/chat-messages.js +6 -0
  95. package/dist/ssr/chat/chat-messages.js.map +1 -1
  96. package/dist/ssr/chat/command-search.js +10 -3
  97. package/dist/ssr/chat/command-search.js.map +1 -1
  98. package/dist/ssr/chat/index.js +6 -1
  99. package/dist/ssr/chat/index.js.map +1 -1
  100. package/dist/ssr/chat/types.js.map +1 -1
  101. package/dist/ssr/content/Calendar.js +3 -3
  102. package/dist/ssr/content/Calendar.js.map +1 -1
  103. package/dist/ssr/content/Chart.js +19 -23
  104. package/dist/ssr/content/Chart.js.map +1 -1
  105. package/dist/ssr/content/FileBrowser.js +2 -2
  106. package/dist/ssr/content/FileBrowser.js.map +1 -1
  107. package/dist/ssr/content/FileTree.js +21 -14
  108. package/dist/ssr/content/FileTree.js.map +1 -1
  109. package/dist/ssr/content/FileTypeIcon.js +50 -0
  110. package/dist/ssr/content/FileTypeIcon.js.map +1 -0
  111. package/dist/ssr/content/FileView.js +2 -2
  112. package/dist/ssr/content/FileView.js.map +1 -1
  113. package/dist/ssr/content/ReferenceChip.js +121 -0
  114. package/dist/ssr/content/ReferenceChip.js.map +1 -0
  115. package/dist/ssr/content/calendar-pointer.js +9 -0
  116. package/dist/ssr/content/calendar-pointer.js.map +1 -1
  117. package/dist/ssr/content/calendar-time-view.js +6 -3
  118. package/dist/ssr/content/calendar-time-view.js.map +1 -1
  119. package/dist/ssr/content/index.js +2 -0
  120. package/dist/ssr/content/index.js.map +1 -1
  121. package/dist/ssr/layout/AppOverview.js +1 -1
  122. package/dist/ssr/layout/AppOverview.js.map +1 -1
  123. package/dist/ssr/layout/AppWorkspace.js +272 -95
  124. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  125. package/dist/ssr/layout/PanelDialog.js +7 -6
  126. package/dist/ssr/layout/PanelDialog.js.map +1 -1
  127. package/dist/ssr/layout/ResizeSeparator.js +45 -0
  128. package/dist/ssr/layout/ResizeSeparator.js.map +1 -0
  129. package/dist/ssr/layout/SidebarSwitcher.js +9 -2
  130. package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
  131. package/dist/ssr/layout/TabBar.js +1 -1
  132. package/dist/ssr/layout/TabBar.js.map +1 -1
  133. package/dist/ssr/layout/app-workspace-controller.js +130 -17
  134. package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
  135. package/dist/ssr/layout/index.js +3 -1
  136. package/dist/ssr/layout/index.js.map +1 -1
  137. package/dist/ssr/layout/resize-separator.js +90 -0
  138. package/dist/ssr/layout/resize-separator.js.map +1 -0
  139. package/dist/ssr/layout/workspace-messages.js +12 -2
  140. package/dist/ssr/layout/workspace-messages.js.map +1 -1
  141. package/dist/ssr/surfaces/LinkCard.js +1 -1
  142. package/dist/ssr/surfaces/LinkCard.js.map +1 -1
  143. package/dist/ssr/surfaces/Placeholder.js +4 -2
  144. package/dist/ssr/surfaces/Placeholder.js.map +1 -1
  145. package/dist/ssr/surfaces/StatCell.js +23 -0
  146. package/dist/ssr/surfaces/StatCell.js.map +1 -1
  147. package/dist/ssr/widgets/Widget.js +1 -1
  148. package/dist/ssr/widgets/Widget.js.map +1 -1
  149. package/dist/styles.css +1 -1
  150. package/dist/types/actions/ContextMenu.d.ts +1 -0
  151. package/dist/types/actions/DragAndDrop.d.ts +29 -0
  152. package/dist/types/actions/drag-and-drop-messages.d.ts +35 -0
  153. package/dist/types/actions/drag-and-drop.d.ts +36 -0
  154. package/dist/types/actions/index.d.ts +2 -0
  155. package/dist/types/chat/ChatComposer.d.ts +19 -1
  156. package/dist/types/chat/ChatDecision.d.ts +133 -0
  157. package/dist/types/chat/ChatPrimitives.d.ts +4 -1
  158. package/dist/types/chat/ChatTimeline.d.ts +2 -1
  159. package/dist/types/chat/chat-messages.d.ts +3 -0
  160. package/dist/types/chat/command-search.d.ts +3 -0
  161. package/dist/types/chat/index.d.ts +23 -1
  162. package/dist/types/chat/types.d.ts +54 -1
  163. package/dist/types/content/Chart.d.ts +9 -3
  164. package/dist/types/content/FileTypeIcon.d.ts +27 -0
  165. package/dist/types/content/ReferenceChip.d.ts +49 -0
  166. package/dist/types/content/calendar-pointer.d.ts +6 -0
  167. package/dist/types/content/index.d.ts +3 -0
  168. package/dist/types/layout/AppWorkspace.d.ts +5 -3
  169. package/dist/types/layout/ResizeSeparator.d.ts +9 -0
  170. package/dist/types/layout/TabBar.d.ts +7 -0
  171. package/dist/types/layout/app-workspace-controller.d.ts +2 -0
  172. package/dist/types/layout/index.d.ts +3 -1
  173. package/dist/types/layout/resize-separator.d.ts +18 -0
  174. package/dist/types/layout/workspace-messages.d.ts +5 -0
  175. package/dist/types/surfaces/StatCell.d.ts +6 -0
  176. package/package.json +2 -2
@@ -0,0 +1,692 @@
1
+ import { createSignal } from "solid-js";
2
+ import { suppressTextSelection } from "../internal/text-selection";
3
+ const SLOP = 10;
4
+ const IGNORE = "input, textarea, select, [contenteditable]:not([contenteditable='false']), [data-drag-ignore]";
5
+ const HOLD_MS = 500;
6
+ const AUTOSCROLL_EDGE = 32;
7
+ const [readActiveDrag, setActiveDrag] = createSignal();
8
+ /** Reactive payload of the drag in progress, or undefined. */
9
+ export const activeDrag = () => readActiveDrag();
10
+ const dragStartListeners = new Set();
11
+ /** Internal hand-over to the gesture menu that owns this press. */
12
+ export const onPointerDragStart = listener => {
13
+ dragStartListeners.add(listener);
14
+ return () => dragStartListeners.delete(listener);
15
+ };
16
+ const engines = new WeakMap();
17
+ const engineFor = doc => {
18
+ let engine = engines.get(doc);
19
+ if (!engine) {
20
+ engine = {
21
+ targets: new Set(),
22
+ byElement: new WeakMap()
23
+ };
24
+ engines.set(doc, engine);
25
+ }
26
+ return engine;
27
+ };
28
+ const accepts = (target, payload) => target.element.isConnected && !target.disabled && target.accept.includes(payload.type);
29
+ const isModal = dialog => {
30
+ try {
31
+ return dialog.matches(":modal");
32
+ } catch {
33
+ return dialog.matches("dialog[open]");
34
+ }
35
+ };
36
+ /** The modal dialog around an element; while one is open, everything outside of it is inert. */
37
+ const modalOf = element => {
38
+ for (let dialog = element.closest("dialog"); dialog; dialog = dialog.parentElement?.closest("dialog") ?? null) if (isModal(dialog)) return dialog;
39
+ return undefined;
40
+ };
41
+ const visible = element => typeof element.checkVisibility === "function" ? element.checkVisibility({
42
+ checkVisibilityCSS: true
43
+ }) : element.getClientRects().length > 0 && getComputedStyle(element).visibility !== "hidden";
44
+ /**
45
+ * A keyboard destination must be one a pointer could reach from where the pickup started: shown and visible, not
46
+ * inert, and in the same modal dialog as the origin.
47
+ */
48
+ const reachable = (target, origin) => {
49
+ const element = target.element;
50
+ if (element.closest("[inert]") || modalOf(element) !== modalOf(origin)) return false;
51
+ return visible(element);
52
+ };
53
+ const mark = (target, payload) => {
54
+ if (payload && accepts(target, payload)) target.element.dataset.drop = "ok";else delete target.element.dataset.drop;
55
+ };
56
+ const containerFor = origin => {
57
+ try {
58
+ return origin.closest("dialog[open]:modal") ?? origin.ownerDocument.body;
59
+ } catch {
60
+ return origin.closest("dialog[open]") ?? origin.ownerDocument.body;
61
+ }
62
+ };
63
+ /** A live region needs a moment between being inserted and being written to, or its first text goes unread. */
64
+ const ANNOUNCE_DELAY_MS = 100;
65
+ /** One live region per container: the body, or the modal dialog the drag happens in, as outside of it is inert. */
66
+ const announcers = new WeakMap();
67
+ const announcerFor = origin => {
68
+ const parent = containerFor(origin);
69
+ let announcer = announcers.get(parent);
70
+ if (!announcer || announcer.region.parentElement !== parent) {
71
+ const region = origin.ownerDocument.createElement("div");
72
+ region.className = "k2b-drag-announcer";
73
+ region.setAttribute("role", "status");
74
+ region.setAttribute("aria-live", "polite");
75
+ region.setAttribute("aria-atomic", "true");
76
+ Object.assign(region.style, {
77
+ position: "absolute",
78
+ width: "1px",
79
+ height: "1px",
80
+ clip: "rect(0, 0, 0, 0)",
81
+ overflow: "hidden",
82
+ whiteSpace: "nowrap"
83
+ });
84
+ // First in a dialog: a dialog stack keeps its top level as the dialog's last child, and finds it there.
85
+ if (parent === origin.ownerDocument.body) parent.append(region);else parent.prepend(region);
86
+ announcer = {
87
+ region,
88
+ ready: performance.now() + ANNOUNCE_DELAY_MS,
89
+ queue: []
90
+ };
91
+ announcers.set(parent, announcer);
92
+ }
93
+ return announcer;
94
+ };
95
+ const announce = (origin, text) => {
96
+ const announcer = announcerFor(origin);
97
+ const write = next => {
98
+ announcer.region.textContent = announcer.region.textContent === next ? `${next}\u00a0` : next;
99
+ };
100
+ const wait = announcer.ready - performance.now();
101
+ if (wait <= 0 && !announcer.queue.length) {
102
+ write(text);
103
+ return;
104
+ }
105
+ // Texts that come before the region is ready are read together once it is, so none replaces another unread.
106
+ if (announcer.queue.push(text) > 1) return;
107
+ origin.ownerDocument.defaultView.setTimeout(() => write(announcer.queue.splice(0).join(" ")), Math.max(0, wait));
108
+ };
109
+ export const registerDropTarget = (element, initial) => {
110
+ const engine = engineFor(element.ownerDocument);
111
+ const entry = {
112
+ element,
113
+ ...initial
114
+ };
115
+ // The live region exists long before the first drag, so its first announcement is read.
116
+ announcerFor(element);
117
+ engine.targets.add(entry);
118
+ engine.byElement.set(element, entry);
119
+ mark(entry, engine.session?.payload);
120
+ engine.session?.targetsChanged?.();
121
+ return {
122
+ update(next) {
123
+ Object.assign(entry, next);
124
+ mark(entry, engine.session?.payload);
125
+ engine.session?.targetsChanged?.();
126
+ },
127
+ dispose() {
128
+ engine.targets.delete(entry);
129
+ engine.byElement.delete(element);
130
+ delete element.dataset.drop;
131
+ engine.session?.targetsChanged?.();
132
+ }
133
+ };
134
+ };
135
+ const clearSession = (engine, session, source) => {
136
+ if (engine.session !== session) return;
137
+ engine.session = undefined;
138
+ if (source) delete source.dataset.dragging;
139
+ for (const target of engine.targets) mark(target);
140
+ setActiveDrag(undefined);
141
+ };
142
+
143
+ /**
144
+ * Suppresses the click and touchend that follow the release of a drag or a hold. A drag cancelled while the button is
145
+ * still down waits for that pointer's release; a new press or a cancelled pointer drops the wait.
146
+ */
147
+ const swallowRelease = (doc, pointerId) => {
148
+ const win = doc.defaultView;
149
+ if (pointerId !== undefined) {
150
+ const stop = () => {
151
+ win.removeEventListener("pointerup", released, true);
152
+ win.removeEventListener("pointercancel", gone, true);
153
+ win.removeEventListener("pointerdown", gone, true);
154
+ };
155
+ const released = event => {
156
+ if (event.pointerId !== pointerId) return;
157
+ stop();
158
+ swallowRelease(doc);
159
+ };
160
+ const gone = event => {
161
+ if (event.type === "pointerdown" || event.pointerId === pointerId) stop();
162
+ };
163
+ win.addEventListener("pointerup", released, true);
164
+ win.addEventListener("pointercancel", gone, true);
165
+ win.addEventListener("pointerdown", gone, true);
166
+ return;
167
+ }
168
+ const click = event => {
169
+ event.preventDefault();
170
+ event.stopImmediatePropagation();
171
+ win.removeEventListener("click", click, true);
172
+ };
173
+ const touchEnd = event => event.preventDefault();
174
+ win.addEventListener("click", click, true);
175
+ win.addEventListener("touchend", touchEnd, {
176
+ capture: true,
177
+ passive: false
178
+ });
179
+ win.setTimeout(() => {
180
+ win.removeEventListener("click", click, true);
181
+ win.removeEventListener("touchend", touchEnd, true);
182
+ }, 0);
183
+ };
184
+
185
+ /** Walks ancestors only; neither hit testing nor scrolling scans the target registry. */
186
+ const SCROLLS = /^(auto|scroll|overlay)$/;
187
+ const LOCKED = /^(hidden|clip)$/;
188
+ /**
189
+ * Scrolls the first container under the pointer that can still scroll toward the edge the pointer rests near. The
190
+ * walk stops at a dialog, so the page behind a modal never scrolls, and the viewport scrolls only while the page is
191
+ * not locked, as `dialogCore` locks it with `overflow: hidden`.
192
+ */
193
+ const scrollAt = (doc, hit, x, y, cache) => {
194
+ const win = doc.defaultView;
195
+ const root = doc.scrollingElement;
196
+ const overflowOf = element => {
197
+ let overflow = cache.get(element);
198
+ if (!overflow) {
199
+ const style = win.getComputedStyle(element);
200
+ overflow = {
201
+ x: style.overflowX,
202
+ y: style.overflowY
203
+ };
204
+ cache.set(element, overflow);
205
+ }
206
+ return overflow;
207
+ };
208
+ for (let element = hit; element; element = element.parentElement) {
209
+ const viewport = element === root;
210
+ let canX;
211
+ let canY;
212
+ if (viewport) {
213
+ // The body's overflow applies to the viewport while the root's is visible.
214
+ const own = overflowOf(element);
215
+ const body = doc.body ? overflowOf(doc.body) : own;
216
+ canX = !LOCKED.test(own.x) && !(own.x === "visible" && LOCKED.test(body.x));
217
+ canY = !LOCKED.test(own.y) && !(own.y === "visible" && LOCKED.test(body.y));
218
+ } else {
219
+ const own = overflowOf(element);
220
+ canX = SCROLLS.test(own.x);
221
+ canY = SCROLLS.test(own.y);
222
+ }
223
+ const rect = viewport ? {
224
+ left: 0,
225
+ top: 0,
226
+ right: win.innerWidth,
227
+ bottom: win.innerHeight
228
+ } : element.getBoundingClientRect();
229
+ const step = (point, start, end, position, max) => {
230
+ if (point < start || point > end) return 0;
231
+ const before = point - start;
232
+ const after = end - point;
233
+ if (before < AUTOSCROLL_EDGE && position > 0) return -Math.max(1, Math.ceil(12 * (1 - before / AUTOSCROLL_EDGE)));
234
+ if (after < AUTOSCROLL_EDGE && position < max) return Math.max(1, Math.ceil(12 * (1 - after / AUTOSCROLL_EDGE)));
235
+ return 0;
236
+ };
237
+ const dx = canX ? step(x, rect.left, rect.right, element.scrollLeft, element.scrollWidth - (viewport ? win.innerWidth : element.clientWidth)) : 0;
238
+ const dy = canY ? step(y, rect.top, rect.bottom, element.scrollTop, element.scrollHeight - (viewport ? win.innerHeight : element.clientHeight)) : 0;
239
+ if (dx || dy) {
240
+ const beforeX = element.scrollLeft;
241
+ const beforeY = element.scrollTop;
242
+ element.scrollLeft += dx;
243
+ element.scrollTop += dy;
244
+ if (element.scrollLeft !== beforeX || element.scrollTop !== beforeY) return true;
245
+ }
246
+ if (element instanceof HTMLDialogElement) return false;
247
+ }
248
+ return false;
249
+ };
250
+ export const bindDragSource = (source, read) => {
251
+ const doc = source.ownerDocument;
252
+ const win = doc.defaultView;
253
+ const engine = engineFor(doc);
254
+ let abandon;
255
+ /** Set while a held finger or a drag owns the touch, so the page must not scroll. */
256
+ let blockTouch = false;
257
+ /**
258
+ * Bound for the source's whole life: WebKit decides at touchstart whether a touch can still be kept from scrolling,
259
+ * and only a non-passive listener present by then lets the later hold do so.
260
+ */
261
+ const preventScroll = event => {
262
+ if (blockTouch && event.cancelable) event.preventDefault();
263
+ };
264
+ /**
265
+ * Fields, editable text, and marked regions inside the source keep their own press and native drag, and so does a
266
+ * whole source inside editable content, such as a paragraph in an editor.
267
+ */
268
+ const ignored = target => {
269
+ // An SVG part has no editable state of its own; its nearest HTML ancestor tells.
270
+ let html = target instanceof Element ? target : null;
271
+ while (html && !(html instanceof HTMLElement)) html = html.parentElement;
272
+ if (html instanceof HTMLElement && html.isContentEditable) return true;
273
+ const found = target instanceof Element ? target.closest(IGNORE) : null;
274
+ return found !== null && source.contains(found);
275
+ };
276
+ const nativeDrag = event => {
277
+ if (!ignored(event.target)) event.preventDefault();
278
+ };
279
+ const down = event => {
280
+ if (!event.isPrimary || event.button !== 0 || engine.session || read().disabled) return;
281
+ if (!(event.target instanceof Element) || event.target.closest(".k2b-drag-source") !== source) return;
282
+ if (ignored(event.target)) return;
283
+ engine.abandonPress?.();
284
+ // Ready before the drag starts, so its first announcement is read.
285
+ announcerFor(source);
286
+ const id = event.pointerId;
287
+ const mouse = event.pointerType === "mouse";
288
+ const start = {
289
+ x: event.clientX,
290
+ y: event.clientY
291
+ };
292
+ let point = start;
293
+ let held = false;
294
+ let finished = false;
295
+ /** The pointer went up or away, so no click of this press is still to come. */
296
+ let released = false;
297
+ let session;
298
+ let messages;
299
+ let preview;
300
+ let releaseSelection;
301
+ let frame = 0;
302
+ let timer;
303
+ let over;
304
+ let refused;
305
+ const overflowCache = new WeakMap();
306
+ const hold = () => {
307
+ if (held || finished) return;
308
+ win.clearTimeout(timer);
309
+ if (read().disabled) {
310
+ cancel();
311
+ return;
312
+ }
313
+ held = true;
314
+ blockTouch = true;
315
+ source.dataset.held = "";
316
+ win.addEventListener("keydown", key, true);
317
+ };
318
+ // Android reports a long press as a context-menu event, which may come before the timer and opens a GestureMenu
319
+ // around the source then. It counts as the hold, so the following move drags.
320
+ const preventContext = event => {
321
+ event.preventDefault();
322
+ if (!session) hold();
323
+ };
324
+ const clean = () => {
325
+ if (finished) return;
326
+ finished = true;
327
+ win.clearTimeout(timer);
328
+ win.cancelAnimationFrame(frame);
329
+ win.removeEventListener("pointermove", move, true);
330
+ win.removeEventListener("pointerup", up, true);
331
+ win.removeEventListener("pointercancel", cancelPointer, true);
332
+ win.removeEventListener("pointerdown", other, true);
333
+ win.removeEventListener("blur", cancel);
334
+ win.removeEventListener("keydown", key, true);
335
+ win.removeEventListener("contextmenu", preventContext, true);
336
+ blockTouch = false;
337
+ source.removeEventListener("lostpointercapture", lostCapture);
338
+ if (source.hasPointerCapture?.(id)) source.releasePointerCapture(id);
339
+ delete source.dataset.held;
340
+ delete source.dataset.pressing;
341
+ preview?.remove();
342
+ releaseSelection?.();
343
+ if (engine.abandonPress === cancel) engine.abandonPress = undefined;
344
+ if (abandon === cancel) abandon = undefined;
345
+ if (session) {
346
+ clearSession(engine, session, source);
347
+ swallowRelease(doc, released ? undefined : id);
348
+ } else if (held) swallowRelease(doc, released ? undefined : id);
349
+ };
350
+ const cancel = () => {
351
+ if (finished) return;
352
+ const payload = session?.payload;
353
+ clean();
354
+ if (payload) announce(source, messages.cancelled(payload));
355
+ };
356
+ const hitTest = () => {
357
+ const payload = session.payload;
358
+ const hit = doc.elementFromPoint(point.x, point.y);
359
+ let inner;
360
+ let next;
361
+ for (let element = hit; element; element = element.parentElement) {
362
+ const target = engine.byElement.get(element);
363
+ // A dialog is a boundary: a target around it lies behind it.
364
+ if (!target && element instanceof HTMLDialogElement) break;
365
+ if (!target) continue;
366
+ inner ??= target;
367
+ if (accepts(target, payload)) {
368
+ next = target;
369
+ break;
370
+ }
371
+ }
372
+ const rejected = next ? undefined : inner;
373
+ if (next !== over || rejected !== refused) {
374
+ if (over) mark(over, engine.targets.has(over) ? payload : undefined);
375
+ if (refused) mark(refused, engine.targets.has(refused) ? payload : undefined);
376
+ over = next;
377
+ refused = rejected;
378
+ if (over) {
379
+ over.element.dataset.drop = "over";
380
+ announce(source, over.messages.over({
381
+ target: over.label
382
+ }));
383
+ } else if (refused) {
384
+ refused.element.dataset.drop = "refused";
385
+ announce(source, refused.messages.refused({
386
+ target: refused.label,
387
+ label: payload.label
388
+ }));
389
+ }
390
+ }
391
+ if (over) over.element.dataset.drop = "over";
392
+ if (refused) refused.element.dataset.drop = "refused";
393
+ return hit;
394
+ };
395
+ const renderFrame = () => {
396
+ frame = 0;
397
+ if (finished || !session) return;
398
+ preview.style.transform = `translate3d(${point.x}px, ${point.y}px, 0)`;
399
+ const hit = hitTest();
400
+ if (scrollAt(doc, hit, point.x, point.y, overflowCache)) frame = win.requestAnimationFrame(renderFrame);
401
+ };
402
+ const begin = () => {
403
+ const current = read();
404
+ if (current.disabled || engine.session) {
405
+ cancel();
406
+ return;
407
+ }
408
+ const payload = {
409
+ ...current.payload
410
+ };
411
+ messages = current.messages;
412
+ session = {
413
+ payload,
414
+ origin: source,
415
+ cancel,
416
+ targetsChanged: () => {
417
+ if (!frame) frame = win.requestAnimationFrame(renderFrame);
418
+ }
419
+ };
420
+ engine.session = session;
421
+ for (const listener of dragStartListeners) listener(id);
422
+ if (finished) return;
423
+ delete source.dataset.held;
424
+ source.dataset.dragging = "";
425
+ win.addEventListener("contextmenu", preventContext, true);
426
+ releaseSelection = suppressTextSelection(id);
427
+ // Captured, the mouse's moves and release reach this drag also over an iframe, such as a PDF preview. A finger
428
+ // or pen is captured by the browser already.
429
+ if (mouse) try {
430
+ source.setPointerCapture(id);
431
+ source.addEventListener("lostpointercapture", lostCapture);
432
+ } catch {
433
+ // The pointer is already gone; its pointerup or pointercancel ends the drag.
434
+ }
435
+ preview = doc.createElement("div");
436
+ // Inside the source's own .k2b-ui scope the preview inherits its theme, also a scoped dark one; a second
437
+ // .k2b-ui would reset the tokens to light.
438
+ const scope = source.parentElement?.closest(".k2b-ui");
439
+ preview.className = scope ? "k2b-drag-preview" : "k2b-ui k2b-drag-preview";
440
+ preview.setAttribute("aria-hidden", "true");
441
+ const icon = doc.createElement("i");
442
+ icon.className = payload.icon ?? "ti ti-file";
443
+ const label = doc.createElement("span");
444
+ label.className = "k2b-drag-preview__label";
445
+ label.textContent = payload.label;
446
+ preview.append(icon, label);
447
+ if (payload.count && payload.count > 1) {
448
+ const count = doc.createElement("span");
449
+ count.className = "k2b-drag-preview__count";
450
+ count.textContent = String(payload.count);
451
+ preview.append(count);
452
+ }
453
+ // In the top layer the preview is placed against the viewport, also inside a transformed dialog, and stays above it.
454
+ preview.setAttribute("popover", "manual");
455
+ (scope ?? doc.body).append(preview);
456
+ preview.showPopover?.();
457
+ preview.style.transform = `translate3d(${point.x}px, ${point.y}px, 0)`;
458
+ for (const target of engine.targets) mark(target, payload);
459
+ setActiveDrag(payload);
460
+ if (finished) return;
461
+ announce(source, messages.pickedUp(payload));
462
+ win.addEventListener("keydown", key, true);
463
+ };
464
+ const move = event => {
465
+ if (event.pointerId !== id || finished) return;
466
+ point = {
467
+ x: event.clientX,
468
+ y: event.clientY
469
+ };
470
+ if (!session && Math.hypot(point.x - start.x, point.y - start.y) >= SLOP) {
471
+ if (event.pointerType !== "mouse" && !held) {
472
+ cancel();
473
+ return;
474
+ }
475
+ begin();
476
+ }
477
+ if (session && !finished && !frame) frame = win.requestAnimationFrame(renderFrame);
478
+ };
479
+ const up = event => {
480
+ if (event.pointerId !== id || finished) return;
481
+ released = true;
482
+ if (!session) {
483
+ clean();
484
+ return;
485
+ }
486
+ point = {
487
+ x: event.clientX,
488
+ y: event.clientY
489
+ };
490
+ hitTest();
491
+ const target = over;
492
+ const payload = session.payload;
493
+ const result = target ? target.messages.dropped({
494
+ label: payload.label,
495
+ target: target.label
496
+ }) : messages.cancelled(payload);
497
+ const drop = target?.drop;
498
+ clean();
499
+ announce(source, result);
500
+ drop?.(payload);
501
+ };
502
+ // Capture ends without a release only when the browser takes the pointer away.
503
+ const lostCapture = event => {
504
+ if (event.pointerId === id) cancel();
505
+ };
506
+ const cancelPointer = event => {
507
+ if (event.pointerId !== id) return;
508
+ released = true;
509
+ cancel();
510
+ };
511
+ const other = event => {
512
+ if (event.pointerId !== id) cancel();
513
+ };
514
+ const key = event => {
515
+ if (event.key !== "Escape") return;
516
+ // A hold is not a drag yet: Escape ends it and still reaches the menu the hold opened.
517
+ if (!session) {
518
+ cancel();
519
+ return;
520
+ }
521
+ event.preventDefault();
522
+ event.stopPropagation();
523
+ cancel();
524
+ };
525
+ abandon = cancel;
526
+ engine.abandonPress = cancel;
527
+ win.addEventListener("pointermove", move, true);
528
+ win.addEventListener("pointerup", up, true);
529
+ win.addEventListener("pointercancel", cancelPointer, true);
530
+ win.addEventListener("pointerdown", other, true);
531
+ win.addEventListener("blur", cancel);
532
+ if (event.pointerType !== "mouse") {
533
+ // A held finger drags: it selects no text, and Android's long-press menu does not open, also before the hold fires.
534
+ source.dataset.pressing = "";
535
+ win.addEventListener("contextmenu", preventContext, true);
536
+ timer = win.setTimeout(hold, HOLD_MS);
537
+ }
538
+ };
539
+ announcerFor(source);
540
+ source.addEventListener("pointerdown", down);
541
+ source.addEventListener("dragstart", nativeDrag);
542
+ source.addEventListener("touchmove", preventScroll, {
543
+ passive: false
544
+ });
545
+ return () => {
546
+ abandon?.();
547
+ if (engine.session && source.contains(engine.session.origin)) engine.session.cancel();
548
+ source.removeEventListener("pointerdown", down);
549
+ source.removeEventListener("dragstart", nativeDrag);
550
+ source.removeEventListener("touchmove", preventScroll);
551
+ };
552
+ };
553
+ const FOCUSABLE = "a[href], button, input, select, textarea, [tabindex]";
554
+ /** The origin itself when it takes focus, else the nearest ancestor that does, such as a menu's group. */
555
+ const focusTarget = origin => (origin.matches(FOCUSABLE) ? origin : origin.closest(FOCUSABLE)) ?? origin;
556
+ export const startKeyboardDrag = (input, origin, messages) => {
557
+ const doc = origin.ownerDocument;
558
+ const win = doc.defaultView;
559
+ const engine = engineFor(doc);
560
+ engine.session?.cancel();
561
+ engine.abandonPress?.();
562
+ const payload = {
563
+ ...input
564
+ };
565
+ const choices = () => [...engine.targets].filter(target => accepts(target, payload) && reachable(target, origin)).sort((a, b) => a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
566
+ let targets = [];
567
+ let current;
568
+ /** Listening for keys: the first destination is chosen. */
569
+ let ready = false;
570
+ let finished = false;
571
+ let frame = 0;
572
+ const source = origin.closest(".k2b-drag-source") ?? undefined;
573
+ const end = (target, text) => {
574
+ if (finished) return;
575
+ finished = true;
576
+ win.cancelAnimationFrame(frame);
577
+ doc.removeEventListener("keydown", key, true);
578
+ doc.removeEventListener("pointerdown", cancel, true);
579
+ win.removeEventListener("blur", cancel);
580
+ const result = text ?? (target ? target.messages.dropped({
581
+ label: payload.label,
582
+ target: target.label
583
+ }) : messages.cancelled(payload));
584
+ const drop = target?.drop;
585
+ clearSession(engine, session, source);
586
+ announce(origin, result);
587
+ // Focus scrolls the origin back into view when choosing a destination scrolled it away.
588
+ if (origin.isConnected) focusTarget(origin).focus();
589
+ drop?.(payload);
590
+ };
591
+ const cancel = () => end();
592
+ const pickupModal = modalOf(origin);
593
+ /**
594
+ * Whether a modal dialog opened over the pickup, leaving the origin inert: the key comes from inside another modal,
595
+ * or a modal opened over a pickup that started outside of every modal.
596
+ */
597
+ const behindModal = event => {
598
+ const keyModal = event.target instanceof Element ? modalOf(event.target) : undefined;
599
+ if (keyModal && keyModal !== pickupModal && !keyModal.contains(origin)) return true;
600
+ // A dialog stack that shares one native dialog hides the level below, the origin's, when the next one opens.
601
+ if (pickupModal && (!origin.isConnected || !visible(origin))) return true;
602
+ return !pickupModal && Array.from(doc.querySelectorAll("dialog[open]")).some(isModal);
603
+ };
604
+ const describe = target => target.messages.choice({
605
+ target: target.label,
606
+ index: targets.indexOf(target) + 1,
607
+ count: targets.length
608
+ });
609
+ const choose = (target, text = describe(target)) => {
610
+ if (current) mark(current, engine.targets.has(current) ? payload : undefined);
611
+ current = target;
612
+ current.element.dataset.drop = "over";
613
+ current.element.scrollIntoView({
614
+ block: "nearest",
615
+ inline: "nearest"
616
+ });
617
+ announce(origin, text);
618
+ };
619
+ const key = event => {
620
+ const backward = event.key === "ArrowUp" || event.key === "ArrowLeft" || event.key === "Tab" && event.shiftKey;
621
+ const forward = !backward && (event.key === "ArrowDown" || event.key === "ArrowRight" || event.key === "Tab");
622
+ if (!backward && !forward && !["Home", "End", "Enter", " ", "Escape"].includes(event.key)) return;
623
+ // A modal dialog opened since the pickup: its keys are its own, and the pickup behind it ends.
624
+ if (behindModal(event)) {
625
+ cancel();
626
+ return;
627
+ }
628
+ event.preventDefault();
629
+ event.stopPropagation();
630
+ if (event.key === "Escape") {
631
+ cancel();
632
+ return;
633
+ }
634
+ // Destinations may have been hidden or shown since the last key, for example by a layout change.
635
+ targets = choices();
636
+ if (!targets.length) {
637
+ end(undefined, messages.noTarget(payload));
638
+ return;
639
+ }
640
+ const index = current ? targets.indexOf(current) : -1;
641
+ const count = targets.length;
642
+ if (forward) choose(targets[index < 0 ? 0 : (index + 1) % count]);else if (backward) choose(targets[index < 0 ? count - 1 : (index - 1 + count) % count]);else if (event.key === "Home") choose(targets[0]);else if (event.key === "End") choose(targets[count - 1]);else end(index >= 0 ? current : undefined);
643
+ };
644
+ /** Chooses the first destination and starts listening; without one, the sequence ends at once. */
645
+ const settle = () => {
646
+ targets = choices();
647
+ if (!targets.length) {
648
+ end(undefined, messages.noTarget(payload));
649
+ return;
650
+ }
651
+ ready = true;
652
+ // One announcement, so a screen reader does not drop the instructions for the first choice.
653
+ choose(targets[0], `${messages.pickedUpKeyboard({
654
+ label: payload.label,
655
+ count: targets.length
656
+ })} ${describe(targets[0])}`);
657
+ // A menu's activating key has already traversed capture. Defer the listener until that dispatch has ended.
658
+ queueMicrotask(() => {
659
+ if (finished) return;
660
+ doc.addEventListener("keydown", key, true);
661
+ doc.addEventListener("pointerdown", cancel, true);
662
+ win.addEventListener("blur", cancel);
663
+ });
664
+ };
665
+ const session = {
666
+ payload,
667
+ origin,
668
+ cancel,
669
+ // Settled a frame later, so a target that a re-render replaces has registered again before none counts as left.
670
+ targetsChanged() {
671
+ if (!ready || finished || frame) return;
672
+ frame = win.requestAnimationFrame(() => {
673
+ frame = 0;
674
+ if (finished) return;
675
+ targets = choices();
676
+ if (!targets.length) cancel();else choose(current && targets.includes(current) ? current : targets[0]);
677
+ });
678
+ }
679
+ };
680
+ // The drag is active before destinations are counted, so a host can mount the ones it shows only while dragging.
681
+ engine.session = session;
682
+ for (const target of engine.targets) mark(target, payload);
683
+ if (source) source.dataset.dragging = "";
684
+ setActiveDrag(payload);
685
+ if (finished) return cancel;
686
+ if (choices().length) settle();else frame = win.requestAnimationFrame(() => {
687
+ frame = 0;
688
+ if (!finished) settle();
689
+ });
690
+ return cancel;
691
+ };
692
+ //# sourceMappingURL=drag-and-drop.js.map