@k2b/ui 0.17.0 → 0.19.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 (228) hide show
  1. package/README.md +3 -2
  2. package/dist/browser/actions/ContextMenu.js +3 -1
  3. package/dist/browser/actions/ContextMenu.js.map +1 -1
  4. package/dist/browser/actions/Dropdown.js +91 -46
  5. package/dist/browser/actions/Dropdown.js.map +1 -1
  6. package/dist/browser/actions/FilterChip.js +4 -1
  7. package/dist/browser/actions/FilterChip.js.map +1 -1
  8. package/dist/browser/chat/ChatComposer.js +1311 -565
  9. package/dist/browser/chat/ChatComposer.js.map +1 -1
  10. package/dist/browser/chat/ChatPrimitives.js +9 -6
  11. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  12. package/dist/browser/chat/chat-behavior.js +9 -0
  13. package/dist/browser/chat/chat-behavior.js.map +1 -1
  14. package/dist/browser/chat/index.js.map +1 -1
  15. package/dist/browser/chat/types.js.map +1 -1
  16. package/dist/browser/content/Calendar.js +579 -533
  17. package/dist/browser/content/Calendar.js.map +1 -1
  18. package/dist/browser/content/FileView.js +62 -36
  19. package/dist/browser/content/FileView.js.map +1 -1
  20. package/dist/browser/content/Lightbox.js +76 -40
  21. package/dist/browser/content/Lightbox.js.map +1 -1
  22. package/dist/browser/content/MarkdownView.js +9 -3
  23. package/dist/browser/content/MarkdownView.js.map +1 -1
  24. package/dist/browser/content/PdfPreview.js +146 -79
  25. package/dist/browser/content/PdfPreview.js.map +1 -1
  26. package/dist/browser/content/Timeline.js +1038 -0
  27. package/dist/browser/content/Timeline.js.map +1 -0
  28. package/dist/browser/content/VideoPlayer.js +299 -0
  29. package/dist/browser/content/VideoPlayer.js.map +1 -0
  30. package/dist/browser/content/chart-inspection.js +7 -2
  31. package/dist/browser/content/chart-inspection.js.map +1 -1
  32. package/dist/browser/content/file-view-preview.js +2 -2
  33. package/dist/browser/content/file-view-preview.js.map +1 -1
  34. package/dist/browser/content/index.js +3 -0
  35. package/dist/browser/content/index.js.map +1 -1
  36. package/dist/browser/content/markdown-links.js +178 -0
  37. package/dist/browser/content/markdown-links.js.map +1 -0
  38. package/dist/browser/content/timeline-model.js +579 -0
  39. package/dist/browser/content/timeline-model.js.map +1 -0
  40. package/dist/browser/feedback/dialog-core.js +26 -14
  41. package/dist/browser/feedback/dialog-core.js.map +1 -1
  42. package/dist/browser/inputs/ChoiceChips.js +66 -0
  43. package/dist/browser/inputs/ChoiceChips.js.map +1 -0
  44. package/dist/browser/inputs/ChoiceGroups.js +53 -12
  45. package/dist/browser/inputs/ChoiceGroups.js.map +1 -1
  46. package/dist/browser/inputs/FileDropTarget.js +129 -0
  47. package/dist/browser/inputs/FileDropTarget.js.map +1 -0
  48. package/dist/browser/inputs/FileInputs.js +84 -43
  49. package/dist/browser/inputs/FileInputs.js.map +1 -1
  50. package/dist/browser/inputs/SignatureInput.js +558 -0
  51. package/dist/browser/inputs/SignatureInput.js.map +1 -0
  52. package/dist/browser/inputs/emoji/EmojiPicker.js +841 -0
  53. package/dist/browser/inputs/emoji/EmojiPicker.js.map +1 -0
  54. package/dist/browser/inputs/emoji/emoji-data.js +1903 -0
  55. package/dist/browser/inputs/emoji/emoji-data.js.map +1 -0
  56. package/dist/browser/inputs/emoji/emoji-index.js +209 -0
  57. package/dist/browser/inputs/emoji/emoji-index.js.map +1 -0
  58. package/dist/browser/inputs/emoji/emoji-messages.js +64 -0
  59. package/dist/browser/inputs/emoji/emoji-messages.js.map +1 -0
  60. package/dist/browser/inputs/file-drop.js +338 -0
  61. package/dist/browser/inputs/file-drop.js.map +1 -0
  62. package/dist/browser/inputs/index.js +6 -0
  63. package/dist/browser/inputs/index.js.map +1 -1
  64. package/dist/browser/inputs/markdown/MarkdownEditor.js +72 -78
  65. package/dist/browser/inputs/markdown/MarkdownEditor.js.map +1 -1
  66. package/dist/browser/inputs/markdown/actions.js +48 -2
  67. package/dist/browser/inputs/markdown/actions.js.map +1 -1
  68. package/dist/browser/inputs/markdown/code-zone.js +38 -2
  69. package/dist/browser/inputs/markdown/code-zone.js.map +1 -1
  70. package/dist/browser/inputs/signature-messages.js +39 -0
  71. package/dist/browser/inputs/signature-messages.js.map +1 -0
  72. package/dist/browser/inputs/signature.js +208 -0
  73. package/dist/browser/inputs/signature.js.map +1 -0
  74. package/dist/browser/internal/render-error.js +49 -0
  75. package/dist/browser/internal/render-error.js.map +1 -0
  76. package/dist/browser/intl/messages.js +205 -4
  77. package/dist/browser/intl/messages.js.map +1 -1
  78. package/dist/browser/layout/AppWorkspace.js +483 -218
  79. package/dist/browser/layout/AppWorkspace.js.map +1 -1
  80. package/dist/browser/layout/FloatingWindow.js +4 -1
  81. package/dist/browser/layout/FloatingWindow.js.map +1 -1
  82. package/dist/browser/layout/Navigation.js +274 -197
  83. package/dist/browser/layout/Navigation.js.map +1 -1
  84. package/dist/browser/layout/SidebarItemPreview.js +5 -1
  85. package/dist/browser/layout/SidebarItemPreview.js.map +1 -1
  86. package/dist/browser/layout/index.js.map +1 -1
  87. package/dist/browser/layout/navigation-model.js +2 -0
  88. package/dist/browser/layout/navigation-model.js.map +1 -1
  89. package/dist/browser/surfaces/NoticeCard.js +47 -44
  90. package/dist/browser/surfaces/NoticeCard.js.map +1 -1
  91. package/dist/browser/surfaces/ResourceCard.js +212 -0
  92. package/dist/browser/surfaces/ResourceCard.js.map +1 -0
  93. package/dist/browser/surfaces/index.js +1 -0
  94. package/dist/browser/surfaces/index.js.map +1 -1
  95. package/dist/licenses/emojibase-data.txt +21 -0
  96. package/dist/ssr/actions/ContextMenu.js +3 -1
  97. package/dist/ssr/actions/ContextMenu.js.map +1 -1
  98. package/dist/ssr/actions/Dropdown.js +49 -12
  99. package/dist/ssr/actions/Dropdown.js.map +1 -1
  100. package/dist/ssr/actions/FilterChip.js +4 -1
  101. package/dist/ssr/actions/FilterChip.js.map +1 -1
  102. package/dist/ssr/chat/ChatComposer.js +823 -221
  103. package/dist/ssr/chat/ChatComposer.js.map +1 -1
  104. package/dist/ssr/chat/ChatPrimitives.js +9 -6
  105. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  106. package/dist/ssr/chat/chat-behavior.js +9 -0
  107. package/dist/ssr/chat/chat-behavior.js.map +1 -1
  108. package/dist/ssr/chat/index.js.map +1 -1
  109. package/dist/ssr/chat/types.js.map +1 -1
  110. package/dist/ssr/content/Calendar.js +183 -142
  111. package/dist/ssr/content/Calendar.js.map +1 -1
  112. package/dist/ssr/content/FileView.js +30 -6
  113. package/dist/ssr/content/FileView.js.map +1 -1
  114. package/dist/ssr/content/Lightbox.js +19 -5
  115. package/dist/ssr/content/Lightbox.js.map +1 -1
  116. package/dist/ssr/content/MarkdownView.js +9 -3
  117. package/dist/ssr/content/MarkdownView.js.map +1 -1
  118. package/dist/ssr/content/PdfPreview.js +77 -36
  119. package/dist/ssr/content/PdfPreview.js.map +1 -1
  120. package/dist/ssr/content/Timeline.js +804 -0
  121. package/dist/ssr/content/Timeline.js.map +1 -0
  122. package/dist/ssr/content/VideoPlayer.js +228 -0
  123. package/dist/ssr/content/VideoPlayer.js.map +1 -0
  124. package/dist/ssr/content/chart-inspection.js +7 -2
  125. package/dist/ssr/content/chart-inspection.js.map +1 -1
  126. package/dist/ssr/content/file-view-preview.js +2 -2
  127. package/dist/ssr/content/file-view-preview.js.map +1 -1
  128. package/dist/ssr/content/index.js +3 -0
  129. package/dist/ssr/content/index.js.map +1 -1
  130. package/dist/ssr/content/markdown-links.js +178 -0
  131. package/dist/ssr/content/markdown-links.js.map +1 -0
  132. package/dist/ssr/content/timeline-model.js +579 -0
  133. package/dist/ssr/content/timeline-model.js.map +1 -0
  134. package/dist/ssr/feedback/dialog-core.js +26 -14
  135. package/dist/ssr/feedback/dialog-core.js.map +1 -1
  136. package/dist/ssr/inputs/ChoiceChips.js +66 -0
  137. package/dist/ssr/inputs/ChoiceChips.js.map +1 -0
  138. package/dist/ssr/inputs/ChoiceGroups.js +12 -4
  139. package/dist/ssr/inputs/ChoiceGroups.js.map +1 -1
  140. package/dist/ssr/inputs/FileDropTarget.js +103 -0
  141. package/dist/ssr/inputs/FileDropTarget.js.map +1 -0
  142. package/dist/ssr/inputs/FileInputs.js +49 -12
  143. package/dist/ssr/inputs/FileInputs.js.map +1 -1
  144. package/dist/ssr/inputs/SignatureInput.js +404 -0
  145. package/dist/ssr/inputs/SignatureInput.js.map +1 -0
  146. package/dist/ssr/inputs/emoji/EmojiPicker.js +551 -0
  147. package/dist/ssr/inputs/emoji/EmojiPicker.js.map +1 -0
  148. package/dist/ssr/inputs/emoji/emoji-data.js +1903 -0
  149. package/dist/ssr/inputs/emoji/emoji-data.js.map +1 -0
  150. package/dist/ssr/inputs/emoji/emoji-index.js +209 -0
  151. package/dist/ssr/inputs/emoji/emoji-index.js.map +1 -0
  152. package/dist/ssr/inputs/emoji/emoji-messages.js +64 -0
  153. package/dist/ssr/inputs/emoji/emoji-messages.js.map +1 -0
  154. package/dist/ssr/inputs/file-drop.js +338 -0
  155. package/dist/ssr/inputs/file-drop.js.map +1 -0
  156. package/dist/ssr/inputs/index.js +6 -0
  157. package/dist/ssr/inputs/index.js.map +1 -1
  158. package/dist/ssr/inputs/markdown/MarkdownEditor.js +14 -2
  159. package/dist/ssr/inputs/markdown/MarkdownEditor.js.map +1 -1
  160. package/dist/ssr/inputs/markdown/actions.js +48 -2
  161. package/dist/ssr/inputs/markdown/actions.js.map +1 -1
  162. package/dist/ssr/inputs/markdown/code-zone.js +38 -2
  163. package/dist/ssr/inputs/markdown/code-zone.js.map +1 -1
  164. package/dist/ssr/inputs/signature-messages.js +39 -0
  165. package/dist/ssr/inputs/signature-messages.js.map +1 -0
  166. package/dist/ssr/inputs/signature.js +208 -0
  167. package/dist/ssr/inputs/signature.js.map +1 -0
  168. package/dist/ssr/internal/render-error.js +49 -0
  169. package/dist/ssr/internal/render-error.js.map +1 -0
  170. package/dist/ssr/intl/messages.js +205 -4
  171. package/dist/ssr/intl/messages.js.map +1 -1
  172. package/dist/ssr/layout/AppWorkspace.js +264 -47
  173. package/dist/ssr/layout/AppWorkspace.js.map +1 -1
  174. package/dist/ssr/layout/FloatingWindow.js +4 -1
  175. package/dist/ssr/layout/FloatingWindow.js.map +1 -1
  176. package/dist/ssr/layout/Navigation.js +169 -122
  177. package/dist/ssr/layout/Navigation.js.map +1 -1
  178. package/dist/ssr/layout/SidebarItemPreview.js +5 -1
  179. package/dist/ssr/layout/SidebarItemPreview.js.map +1 -1
  180. package/dist/ssr/layout/index.js.map +1 -1
  181. package/dist/ssr/layout/navigation-model.js +2 -0
  182. package/dist/ssr/layout/navigation-model.js.map +1 -1
  183. package/dist/ssr/surfaces/NoticeCard.js +17 -12
  184. package/dist/ssr/surfaces/NoticeCard.js.map +1 -1
  185. package/dist/ssr/surfaces/ResourceCard.js +115 -0
  186. package/dist/ssr/surfaces/ResourceCard.js.map +1 -0
  187. package/dist/ssr/surfaces/index.js +1 -0
  188. package/dist/ssr/surfaces/index.js.map +1 -1
  189. package/dist/styles.css +1 -1
  190. package/dist/types/actions/Dropdown.d.ts +13 -1
  191. package/dist/types/actions/FilterChip.d.ts +5 -0
  192. package/dist/types/chat/ChatComposer.d.ts +24 -1
  193. package/dist/types/chat/ChatPrimitives.d.ts +1 -1
  194. package/dist/types/chat/chat-behavior.d.ts +15 -0
  195. package/dist/types/chat/index.d.ts +1 -1
  196. package/dist/types/chat/types.d.ts +34 -0
  197. package/dist/types/content/Calendar.d.ts +28 -8
  198. package/dist/types/content/Lightbox.d.ts +8 -0
  199. package/dist/types/content/Timeline.d.ts +49 -0
  200. package/dist/types/content/VideoPlayer.d.ts +41 -0
  201. package/dist/types/content/index.d.ts +6 -2
  202. package/dist/types/content/markdown-links.d.ts +66 -0
  203. package/dist/types/content/timeline-model.d.ts +148 -0
  204. package/dist/types/feedback/dialog-core.d.ts +1 -1
  205. package/dist/types/inputs/ChoiceChips.d.ts +20 -0
  206. package/dist/types/inputs/ChoiceGroups.d.ts +11 -2
  207. package/dist/types/inputs/FileDropTarget.d.ts +23 -0
  208. package/dist/types/inputs/FileInputs.d.ts +8 -0
  209. package/dist/types/inputs/SignatureInput.d.ts +22 -0
  210. package/dist/types/inputs/emoji/EmojiPicker.d.ts +42 -0
  211. package/dist/types/inputs/emoji/emoji-data.d.ts +2 -0
  212. package/dist/types/inputs/emoji/emoji-index.d.ts +109 -0
  213. package/dist/types/inputs/emoji/emoji-messages.d.ts +30 -0
  214. package/dist/types/inputs/file-drop.d.ts +80 -0
  215. package/dist/types/inputs/index.d.ts +12 -0
  216. package/dist/types/inputs/markdown/actions.d.ts +4 -0
  217. package/dist/types/inputs/markdown/code-zone.d.ts +7 -0
  218. package/dist/types/inputs/signature-messages.d.ts +11 -0
  219. package/dist/types/inputs/signature.d.ts +72 -0
  220. package/dist/types/internal/render-error.d.ts +13 -0
  221. package/dist/types/intl/messages.d.ts +101 -0
  222. package/dist/types/layout/AppWorkspace.d.ts +38 -0
  223. package/dist/types/layout/SidebarItemPreview.d.ts +2 -0
  224. package/dist/types/layout/index.d.ts +2 -2
  225. package/dist/types/layout/navigation-model.d.ts +18 -0
  226. package/dist/types/surfaces/ResourceCard.d.ts +31 -0
  227. package/dist/types/surfaces/index.d.ts +2 -0
  228. package/package.json +5 -4
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The emoji dataset behind `EmojiPicker` and the composer's `:shortcode` completion. The data (about 80 kB compressed)
3
+ * loads on first use, so a page that never opens either does not download it.
4
+ */
5
+
6
+ import { isInCodeZone } from "../markdown/code-zone";
7
+
8
+ /** `0` is the default yellow; `1` (light) to `5` (dark) are the Fitzpatrick modifiers. */
9
+
10
+ export const EMOJI_SKIN_TONES = [0, 1, 2, 3, 4, 5];
11
+
12
+ /** The groups in their order. Emojibase numbers them 0 to 9; 2 holds skin-tone parts and never appears. */
13
+ export const EMOJI_GROUPS = [{
14
+ id: 0,
15
+ key: "smileys",
16
+ icon: "ti ti-mood-smile"
17
+ }, {
18
+ id: 1,
19
+ key: "people",
20
+ icon: "ti ti-hand-stop"
21
+ }, {
22
+ id: 3,
23
+ key: "animals",
24
+ icon: "ti ti-paw"
25
+ }, {
26
+ id: 4,
27
+ key: "food",
28
+ icon: "ti ti-tools-kitchen-2"
29
+ }, {
30
+ id: 5,
31
+ key: "travel",
32
+ icon: "ti ti-plane"
33
+ }, {
34
+ id: 6,
35
+ key: "activities",
36
+ icon: "ti ti-ball-football"
37
+ }, {
38
+ id: 7,
39
+ key: "objects",
40
+ icon: "ti ti-bulb"
41
+ }, {
42
+ id: 8,
43
+ key: "symbols",
44
+ icon: "ti ti-heart"
45
+ }, {
46
+ id: 9,
47
+ key: "flags",
48
+ icon: "ti ti-flag"
49
+ }];
50
+ const LIGHT_TONE = "\u{1F3FB}";
51
+ const PRESENTATION_SELECTOR = /️/g;
52
+
53
+ /** Lower case without accents, so "lacheln", "laecheln" and "Lächeln" meet; `_` and `-` separate words. */
54
+ export const normalizeEmojiQuery = value => value.toLowerCase().normalize("NFD").replace(/\p{M}/gu, "").replace(/ß/g, "ss").replace(/[_\-\s]+/g, " ").trim();
55
+
56
+ /** German also gets its spelling without umlauts: "Lächeln" is found as "laecheln". */
57
+ const germanForms = value => {
58
+ const plain = normalizeEmojiQuery(value);
59
+ const spelled = normalizeEmojiQuery(value.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue"));
60
+ return spelled === plain ? [plain] : [plain, spelled];
61
+ };
62
+ const list = value => value ? value.split("|") : [];
63
+ export const parseEmojiData = data => {
64
+ const entries = [];
65
+ const byEmoji = new Map();
66
+ const byShortcode = new Map();
67
+ for (const line of data.split("\n")) {
68
+ const [emoji, group, light, en, enKeywords, de, deKeywords, codes] = line.split("\t");
69
+ if (!emoji || !en || !de) continue;
70
+ const shortcodes = list(codes);
71
+ const names = [normalizeEmojiQuery(en), ...germanForms(de)];
72
+ const keywords = [...list(enKeywords).map(normalizeEmojiQuery), ...list(deKeywords).flatMap(germanForms)];
73
+ const normalizedCodes = shortcodes.map(normalizeEmojiQuery);
74
+ const entry = {
75
+ emoji,
76
+ group: Number(group),
77
+ light: light || undefined,
78
+ name: {
79
+ en,
80
+ de
81
+ },
82
+ shortcodes,
83
+ search: {
84
+ names,
85
+ keywords,
86
+ shortcodes: normalizedCodes,
87
+ words: [...new Set([...names, ...keywords, ...normalizedCodes].flatMap(term => term.split(" ")))]
88
+ }
89
+ };
90
+ entries.push(entry);
91
+ for (const variant of [emoji, ...EMOJI_SKIN_TONES.slice(1).map(tone => emojiInTone(entry, tone))]) {
92
+ byEmoji.set(variant.replace(PRESENTATION_SELECTOR, ""), entry);
93
+ }
94
+ for (const code of shortcodes) byShortcode.set(code.toLowerCase(), entry);
95
+ }
96
+ return {
97
+ entries,
98
+ byEmoji,
99
+ byShortcode
100
+ };
101
+ };
102
+ let loading;
103
+
104
+ /** Loads and indexes the dataset once. A failed load is retried on the next call. */
105
+ export const loadEmojiIndex = () => {
106
+ loading ??= import("./emoji-data").then(module => parseEmojiData(module.default), error => {
107
+ loading = undefined;
108
+ throw error;
109
+ });
110
+ return loading;
111
+ };
112
+
113
+ /** The entry of an emoji in any skin tone, with or without presentation selectors. */
114
+ export const findEmoji = (index, emoji) => index.byEmoji.get(emoji.replace(PRESENTATION_SELECTOR, ""));
115
+
116
+ /** The emoji in `tone`; emoji without skin tones stay as they are. */
117
+ export const emojiInTone = (entry, tone) => tone === 0 || !entry.light ? entry.emoji : entry.light.replaceAll(LIGHT_TONE, String.fromCodePoint(0x1f3fb + tone - 1));
118
+
119
+ /** The name in the render language: German for `de` locales, English otherwise. */
120
+ export const emojiName = (entry, locale) => /^de\b/i.test(locale) ? entry.name.de : entry.name.en;
121
+
122
+ /**
123
+ * Lower is better; `undefined` does not match. A shortcode's prefix beats a name's, so `:tad` finds 🎉 first. `infix`
124
+ * also matches letters inside a name, as the picker's search does.
125
+ */
126
+ const rank = (entry, query, infix) => {
127
+ const {
128
+ names,
129
+ keywords,
130
+ shortcodes,
131
+ words
132
+ } = entry.search;
133
+ if (shortcodes.includes(query)) return 0;
134
+ if (names.includes(query)) return 1;
135
+ if (shortcodes.some(code => code.startsWith(query))) return 2;
136
+ if (names.some(name => name.startsWith(query))) return 3;
137
+ if (names.some(name => name.includes(` ${query}`))) return 4;
138
+ if (keywords.includes(query)) return 5;
139
+ if (keywords.some(keyword => keyword.startsWith(query))) return 6;
140
+ if (infix && names.some(name => name.includes(query))) return 7;
141
+ // Several words match when each starts a word of the entry: "hand up" finds "raising hand".
142
+ const terms = query.split(" ");
143
+ if (terms.length > 1 && terms.every(term => words.some(word => word.startsWith(term)))) return 8;
144
+ return undefined;
145
+ };
146
+ const ranked = (index, query, infix, limit) => {
147
+ if (!query) return [];
148
+ const matches = [];
149
+ index.entries.forEach((entry, position) => {
150
+ const value = rank(entry, query, infix);
151
+ if (value !== undefined) matches.push({
152
+ entry,
153
+ rank: value,
154
+ position
155
+ });
156
+ });
157
+ matches.sort((left, right) => left.rank - right.rank || left.position - right.position);
158
+ return matches.slice(0, limit).map(match => match.entry);
159
+ };
160
+
161
+ /** Matches in English and German names, keywords and shortcodes, best first and otherwise in Unicode order. */
162
+ export const searchEmoji = (index, query, limit = Number.POSITIVE_INFINITY) => ranked(index, normalizeEmojiQuery(query.replace(/^:+|:+$/g, "")), true, limit);
163
+
164
+ /**
165
+ * The suggestions for a `:shortcode` being typed. Only the start of a shortcode, name, word or keyword counts, and a
166
+ * leading sign must start a shortcode, as in `:+1` and `:-1`: emoticons such as `:DD`, `:-D` or `:-P` find nothing,
167
+ * so Enter still sends them.
168
+ */
169
+ export const suggestEmoji = (index, query, limit) => {
170
+ if (/^[+-]/.test(query)) {
171
+ const code = query.toLowerCase();
172
+ return index.entries.filter(entry => entry.shortcodes.some(shortcode => shortcode.startsWith(code))).slice(0, limit);
173
+ }
174
+ const normalized = normalizeEmojiQuery(query);
175
+ return normalized.length < 2 ? [] : ranked(index, normalized, false, limit);
176
+ };
177
+
178
+ /** As many recent emoji as the picker shows: three rows. */
179
+ export const RECENT_EMOJI_LIMIT = 24;
180
+
181
+ /** The recent list after `emoji` was used: it moves to the front, once, and the oldest drop off. */
182
+ export const rememberEmoji = (recent, emoji) => [emoji, ...recent.filter(item => item !== emoji)].slice(0, RECENT_EMOJI_LIMIT);
183
+ /**
184
+ * The `:shortcode` being typed at the caret: a colon at the start or after a space or "(", then at least two letters,
185
+ * digits, "_", "+" or "-". Code, URLs ("https://") and times ("10:30") are no queries.
186
+ */
187
+ export const emojiShortcodeQuery = (text, caret, selectionEnd = caret) => {
188
+ if (caret !== selectionEnd || isInCodeZone(text, caret)) return null;
189
+ const match = /(?:^|[\s(]):([\p{L}\p{N}_+-]{2,})$/u.exec(text.slice(0, caret));
190
+ if (!match) return null;
191
+ return {
192
+ start: caret - match[1].length - 1,
193
+ end: caret,
194
+ query: match[1]
195
+ };
196
+ };
197
+
198
+ /** A complete `:shortcode:` just typed at the caret, such as `:thumbsup:`, with its range. */
199
+ export const completedEmojiShortcode = (text, caret) => {
200
+ if (isInCodeZone(text, caret)) return null;
201
+ const match = /(?:^|[\s(]):([\p{L}\p{N}_+-]+):$/u.exec(text.slice(0, caret));
202
+ if (!match) return null;
203
+ return {
204
+ start: caret - match[1].length - 2,
205
+ end: caret,
206
+ shortcode: match[1]
207
+ };
208
+ };
209
+ //# sourceMappingURL=emoji-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sources":["inputs/emoji/emoji-index.ts"],"sourcesContent":["/**\n * The emoji dataset behind `EmojiPicker` and the composer's `:shortcode` completion. The data (about 80 kB compressed)\n * loads on first use, so a page that never opens either does not download it.\n */\n\nimport { isInCodeZone } from \"../markdown/code-zone\";\n\n/** `0` is the default yellow; `1` (light) to `5` (dark) are the Fitzpatrick modifiers. */\nexport type EmojiSkinTone = 0 | 1 | 2 | 3 | 4 | 5;\n\nexport const EMOJI_SKIN_TONES: readonly EmojiSkinTone[] = [0, 1, 2, 3, 4, 5];\n\n/** The groups in their order. Emojibase numbers them 0 to 9; 2 holds skin-tone parts and never appears. */\nexport const EMOJI_GROUPS = [\n { id: 0, key: \"smileys\", icon: \"ti ti-mood-smile\" },\n { id: 1, key: \"people\", icon: \"ti ti-hand-stop\" },\n { id: 3, key: \"animals\", icon: \"ti ti-paw\" },\n { id: 4, key: \"food\", icon: \"ti ti-tools-kitchen-2\" },\n { id: 5, key: \"travel\", icon: \"ti ti-plane\" },\n { id: 6, key: \"activities\", icon: \"ti ti-ball-football\" },\n { id: 7, key: \"objects\", icon: \"ti ti-bulb\" },\n { id: 8, key: \"symbols\", icon: \"ti ti-heart\" },\n { id: 9, key: \"flags\", icon: \"ti ti-flag\" },\n] as const;\n\nexport type EmojiGroupKey = (typeof EMOJI_GROUPS)[number][\"key\"];\n\nexport type EmojiEntry = {\n emoji: string;\n group: number;\n /** The light variant; the other tones swap its modifier. Absent for emoji without skin tones. */\n light?: string;\n name: { en: string; de: string };\n shortcodes: readonly string[];\n /** Normalized names, keywords and shortcodes for search. */\n search: { names: readonly string[]; keywords: readonly string[]; shortcodes: readonly string[]; words: readonly string[] };\n};\n\nexport type EmojiIndex = {\n entries: readonly EmojiEntry[];\n /** Every emoji and skin-tone variant, without presentation selectors, to its entry. Look up with `findEmoji`. */\n byEmoji: ReadonlyMap<string, EmojiEntry>;\n byShortcode: ReadonlyMap<string, EmojiEntry>;\n};\n\nconst LIGHT_TONE = \"\\u{1F3FB}\";\nconst PRESENTATION_SELECTOR = /️/g;\n\n/** Lower case without accents, so \"lacheln\", \"laecheln\" and \"Lächeln\" meet; `_` and `-` separate words. */\nexport const normalizeEmojiQuery = (value: string): string =>\n value\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/\\p{M}/gu, \"\")\n .replace(/ß/g, \"ss\")\n .replace(/[_\\-\\s]+/g, \" \")\n .trim();\n\n/** German also gets its spelling without umlauts: \"Lächeln\" is found as \"laecheln\". */\nconst germanForms = (value: string): string[] => {\n const plain = normalizeEmojiQuery(value);\n const spelled = normalizeEmojiQuery(value.toLowerCase().replace(/ä/g, \"ae\").replace(/ö/g, \"oe\").replace(/ü/g, \"ue\"));\n return spelled === plain ? [plain] : [plain, spelled];\n};\n\nconst list = (value: string | undefined): string[] => (value ? value.split(\"|\") : []);\n\nexport const parseEmojiData = (data: string): EmojiIndex => {\n const entries: EmojiEntry[] = [];\n const byEmoji = new Map<string, EmojiEntry>();\n const byShortcode = new Map<string, EmojiEntry>();\n for (const line of data.split(\"\\n\")) {\n const [emoji, group, light, en, enKeywords, de, deKeywords, codes] = line.split(\"\\t\");\n if (!emoji || !en || !de) continue;\n const shortcodes = list(codes);\n const names = [normalizeEmojiQuery(en), ...germanForms(de)];\n const keywords = [...list(enKeywords).map(normalizeEmojiQuery), ...list(deKeywords).flatMap(germanForms)];\n const normalizedCodes = shortcodes.map(normalizeEmojiQuery);\n const entry: EmojiEntry = {\n emoji,\n group: Number(group),\n light: light || undefined,\n name: { en, de },\n shortcodes,\n search: {\n names,\n keywords,\n shortcodes: normalizedCodes,\n words: [...new Set([...names, ...keywords, ...normalizedCodes].flatMap((term) => term.split(\" \")))],\n },\n };\n entries.push(entry);\n for (const variant of [emoji, ...EMOJI_SKIN_TONES.slice(1).map((tone) => emojiInTone(entry, tone))]) {\n byEmoji.set(variant.replace(PRESENTATION_SELECTOR, \"\"), entry);\n }\n for (const code of shortcodes) byShortcode.set(code.toLowerCase(), entry);\n }\n return { entries, byEmoji, byShortcode };\n};\n\nlet loading: Promise<EmojiIndex> | undefined;\n\n/** Loads and indexes the dataset once. A failed load is retried on the next call. */\nexport const loadEmojiIndex = (): Promise<EmojiIndex> => {\n loading ??= import(\"./emoji-data\").then(\n (module) => parseEmojiData(module.default),\n (error: unknown) => {\n loading = undefined;\n throw error;\n },\n );\n return loading;\n};\n\n/** The entry of an emoji in any skin tone, with or without presentation selectors. */\nexport const findEmoji = (index: EmojiIndex, emoji: string): EmojiEntry | undefined =>\n index.byEmoji.get(emoji.replace(PRESENTATION_SELECTOR, \"\"));\n\n/** The emoji in `tone`; emoji without skin tones stay as they are. */\nexport const emojiInTone = (entry: EmojiEntry, tone: EmojiSkinTone): string =>\n tone === 0 || !entry.light ? entry.emoji : entry.light.replaceAll(LIGHT_TONE, String.fromCodePoint(0x1f3fb + tone - 1));\n\n/** The name in the render language: German for `de` locales, English otherwise. */\nexport const emojiName = (entry: EmojiEntry, locale: string): string => (/^de\\b/i.test(locale) ? entry.name.de : entry.name.en);\n\n/**\n * Lower is better; `undefined` does not match. A shortcode's prefix beats a name's, so `:tad` finds 🎉 first. `infix`\n * also matches letters inside a name, as the picker's search does.\n */\nconst rank = (entry: EmojiEntry, query: string, infix: boolean): number | undefined => {\n const { names, keywords, shortcodes, words } = entry.search;\n if (shortcodes.includes(query)) return 0;\n if (names.includes(query)) return 1;\n if (shortcodes.some((code) => code.startsWith(query))) return 2;\n if (names.some((name) => name.startsWith(query))) return 3;\n if (names.some((name) => name.includes(` ${query}`))) return 4;\n if (keywords.includes(query)) return 5;\n if (keywords.some((keyword) => keyword.startsWith(query))) return 6;\n if (infix && names.some((name) => name.includes(query))) return 7;\n // Several words match when each starts a word of the entry: \"hand up\" finds \"raising hand\".\n const terms = query.split(\" \");\n if (terms.length > 1 && terms.every((term) => words.some((word) => word.startsWith(term)))) return 8;\n return undefined;\n};\n\nconst ranked = (index: EmojiIndex, query: string, infix: boolean, limit: number): EmojiEntry[] => {\n if (!query) return [];\n const matches: { entry: EmojiEntry; rank: number; position: number }[] = [];\n index.entries.forEach((entry, position) => {\n const value = rank(entry, query, infix);\n if (value !== undefined) matches.push({ entry, rank: value, position });\n });\n matches.sort((left, right) => left.rank - right.rank || left.position - right.position);\n return matches.slice(0, limit).map((match) => match.entry);\n};\n\n/** Matches in English and German names, keywords and shortcodes, best first and otherwise in Unicode order. */\nexport const searchEmoji = (index: EmojiIndex, query: string, limit = Number.POSITIVE_INFINITY): EmojiEntry[] =>\n ranked(index, normalizeEmojiQuery(query.replace(/^:+|:+$/g, \"\")), true, limit);\n\n/**\n * The suggestions for a `:shortcode` being typed. Only the start of a shortcode, name, word or keyword counts, and a\n * leading sign must start a shortcode, as in `:+1` and `:-1`: emoticons such as `:DD`, `:-D` or `:-P` find nothing,\n * so Enter still sends them.\n */\nexport const suggestEmoji = (index: EmojiIndex, query: string, limit: number): EmojiEntry[] => {\n if (/^[+-]/.test(query)) {\n const code = query.toLowerCase();\n return index.entries.filter((entry) => entry.shortcodes.some((shortcode) => shortcode.startsWith(code))).slice(0, limit);\n }\n const normalized = normalizeEmojiQuery(query);\n return normalized.length < 2 ? [] : ranked(index, normalized, false, limit);\n};\n\n/** As many recent emoji as the picker shows: three rows. */\nexport const RECENT_EMOJI_LIMIT = 24;\n\n/** The recent list after `emoji` was used: it moves to the front, once, and the oldest drop off. */\nexport const rememberEmoji = (recent: readonly string[], emoji: string): string[] =>\n [emoji, ...recent.filter((item) => item !== emoji)].slice(0, RECENT_EMOJI_LIMIT);\n\nexport type EmojiShortcodeQuery = { start: number; end: number; query: string };\n\n/**\n * The `:shortcode` being typed at the caret: a colon at the start or after a space or \"(\", then at least two letters,\n * digits, \"_\", \"+\" or \"-\". Code, URLs (\"https://\") and times (\"10:30\") are no queries.\n */\nexport const emojiShortcodeQuery = (text: string, caret: number, selectionEnd = caret): EmojiShortcodeQuery | null => {\n if (caret !== selectionEnd || isInCodeZone(text, caret)) return null;\n const match = /(?:^|[\\s(]):([\\p{L}\\p{N}_+-]{2,})$/u.exec(text.slice(0, caret));\n if (!match) return null;\n return { start: caret - match[1]!.length - 1, end: caret, query: match[1]! };\n};\n\n/** A complete `:shortcode:` just typed at the caret, such as `:thumbsup:`, with its range. */\nexport const completedEmojiShortcode = (text: string, caret: number): { start: number; end: number; shortcode: string } | null => {\n if (isInCodeZone(text, caret)) return null;\n const match = /(?:^|[\\s(]):([\\p{L}\\p{N}_+-]+):$/u.exec(text.slice(0, caret));\n if (!match) return null;\n return { start: caret - match[1]!.length - 2, end: caret, shortcode: match[1]! };\n};\n"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,64 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+ import { useLocale } from "../../intl/locale";
3
+
4
+ /** The picker's own strings. They live with the picker, so pages without it do not carry them. */
5
+ const emojiMessages = i18n.define({
6
+ baseLocale: "en",
7
+ messages: {
8
+ en: {
9
+ emojiPicker: "Emoji",
10
+ emojiSearch: "Search emoji",
11
+ emojiGroups: "Emoji groups",
12
+ emojiRecent: "Recently used",
13
+ emojiSmileys: "Smileys & emotion",
14
+ emojiPeople: "People & body",
15
+ emojiAnimals: "Animals & nature",
16
+ emojiFood: "Food & drink",
17
+ emojiTravel: "Travel & places",
18
+ emojiActivities: "Activities",
19
+ emojiObjects: "Objects",
20
+ emojiSymbols: "Symbols",
21
+ emojiFlags: "Flags",
22
+ emojiSkinTone: "Skin tone",
23
+ emojiToneDefault: "Default",
24
+ emojiToneLight: "Light",
25
+ emojiToneMediumLight: "Medium-light",
26
+ emojiToneMedium: "Medium",
27
+ emojiToneMediumDark: "Medium-dark",
28
+ emojiToneDark: "Dark",
29
+ emojiNoResults: "No emoji found",
30
+ emojiLoadFailed: "Emoji could not be loaded."
31
+ },
32
+ de: {
33
+ emojiPicker: "Emoji",
34
+ emojiSearch: "Emoji suchen",
35
+ emojiGroups: "Emoji-Gruppen",
36
+ emojiRecent: "Zuletzt verwendet",
37
+ emojiSmileys: "Smileys & Emotionen",
38
+ emojiPeople: "Menschen & Körper",
39
+ emojiAnimals: "Tiere & Natur",
40
+ emojiFood: "Essen & Trinken",
41
+ emojiTravel: "Reisen & Orte",
42
+ emojiActivities: "Aktivitäten",
43
+ emojiObjects: "Gegenstände",
44
+ emojiSymbols: "Symbole",
45
+ emojiFlags: "Flaggen",
46
+ emojiSkinTone: "Hautfarbe",
47
+ emojiToneDefault: "Standard",
48
+ emojiToneLight: "Hell",
49
+ emojiToneMediumLight: "Mittelhell",
50
+ emojiToneMedium: "Mittel",
51
+ emojiToneMediumDark: "Mitteldunkel",
52
+ emojiToneDark: "Dunkel",
53
+ emojiNoResults: "Kein Emoji gefunden",
54
+ emojiLoadFailed: "Emoji konnten nicht geladen werden."
55
+ }
56
+ }
57
+ });
58
+ /** Resolved from the inherited render locale, like the rest of @k2b/ui. */
59
+ export const useEmojiMessages = () => {
60
+ const locale = useLocale();
61
+ return () => emojiMessages.resolve([locale()]).t;
62
+ };
63
+ export const checkEmojiMessages = () => emojiMessages.check();
64
+ //# sourceMappingURL=emoji-messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sources":["inputs/emoji/emoji-messages.ts"],"sourcesContent":["import { i18n } from \"@k2b/stdlib\";\nimport { useLocale } from \"../../intl/locale\";\n\n/** The picker's own strings. They live with the picker, so pages without it do not carry them. */\nconst emojiMessages = i18n.define({\n baseLocale: \"en\",\n messages: {\n en: {\n emojiPicker: \"Emoji\",\n emojiSearch: \"Search emoji\",\n emojiGroups: \"Emoji groups\",\n emojiRecent: \"Recently used\",\n emojiSmileys: \"Smileys & emotion\",\n emojiPeople: \"People & body\",\n emojiAnimals: \"Animals & nature\",\n emojiFood: \"Food & drink\",\n emojiTravel: \"Travel & places\",\n emojiActivities: \"Activities\",\n emojiObjects: \"Objects\",\n emojiSymbols: \"Symbols\",\n emojiFlags: \"Flags\",\n emojiSkinTone: \"Skin tone\",\n emojiToneDefault: \"Default\",\n emojiToneLight: \"Light\",\n emojiToneMediumLight: \"Medium-light\",\n emojiToneMedium: \"Medium\",\n emojiToneMediumDark: \"Medium-dark\",\n emojiToneDark: \"Dark\",\n emojiNoResults: \"No emoji found\",\n emojiLoadFailed: \"Emoji could not be loaded.\",\n },\n de: {\n emojiPicker: \"Emoji\",\n emojiSearch: \"Emoji suchen\",\n emojiGroups: \"Emoji-Gruppen\",\n emojiRecent: \"Zuletzt verwendet\",\n emojiSmileys: \"Smileys & Emotionen\",\n emojiPeople: \"Menschen & Körper\",\n emojiAnimals: \"Tiere & Natur\",\n emojiFood: \"Essen & Trinken\",\n emojiTravel: \"Reisen & Orte\",\n emojiActivities: \"Aktivitäten\",\n emojiObjects: \"Gegenstände\",\n emojiSymbols: \"Symbole\",\n emojiFlags: \"Flaggen\",\n emojiSkinTone: \"Hautfarbe\",\n emojiToneDefault: \"Standard\",\n emojiToneLight: \"Hell\",\n emojiToneMediumLight: \"Mittelhell\",\n emojiToneMedium: \"Mittel\",\n emojiToneMediumDark: \"Mitteldunkel\",\n emojiToneDark: \"Dunkel\",\n emojiNoResults: \"Kein Emoji gefunden\",\n emojiLoadFailed: \"Emoji konnten nicht geladen werden.\",\n },\n },\n});\n\nexport type EmojiMessages = ReturnType<(typeof emojiMessages)[\"resolve\"]>[\"t\"];\n\n/** Resolved from the inherited render locale, like the rest of @k2b/ui. */\nexport const useEmojiMessages = (): (() => EmojiMessages) => {\n const locale = useLocale();\n return () => emojiMessages.resolve([locale()]).t;\n};\n\nexport const checkEmojiMessages = () => emojiMessages.check();\n"],"mappings":"AAAA,SAAS,IAAI,QAAQ,aAAa;AAClC,SAAS,SAAS,QAAQ,mBAAmB;;AAE7C;AACA,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;EAChC,UAAU,EAAE,IAAI;EAChB,QAAQ,EAAE;IACR,EAAE,EAAE;MACF,WAAW,EAAE,OAAO;MACpB,WAAW,EAAE,cAAc;MAC3B,WAAW,EAAE,cAAc;MAC3B,WAAW,EAAE,eAAe;MAC5B,YAAY,EAAE,mBAAmB;MACjC,WAAW,EAAE,eAAe;MAC5B,YAAY,EAAE,kBAAkB;MAChC,SAAS,EAAE,cAAc;MACzB,WAAW,EAAE,iBAAiB;MAC9B,eAAe,EAAE,YAAY;MAC7B,YAAY,EAAE,SAAS;MACvB,YAAY,EAAE,SAAS;MACvB,UAAU,EAAE,OAAO;MACnB,aAAa,EAAE,WAAW;MAC1B,gBAAgB,EAAE,SAAS;MAC3B,cAAc,EAAE,OAAO;MACvB,oBAAoB,EAAE,cAAc;MACpC,eAAe,EAAE,QAAQ;MACzB,mBAAmB,EAAE,aAAa;MAClC,aAAa,EAAE,MAAM;MACrB,cAAc,EAAE,gBAAgB;MAChC,eAAe,EAAE;IACnB,CAAC;IACD,EAAE,EAAE;MACF,WAAW,EAAE,OAAO;MACpB,WAAW,EAAE,cAAc;MAC3B,WAAW,EAAE,eAAe;MAC5B,WAAW,EAAE,mBAAmB;MAChC,YAAY,EAAE,qBAAqB;MACnC,WAAW,EAAE,mBAAmB;MAChC,YAAY,EAAE,eAAe;MAC7B,SAAS,EAAE,iBAAiB;MAC5B,WAAW,EAAE,eAAe;MAC5B,eAAe,EAAE,aAAa;MAC9B,YAAY,EAAE,aAAa;MAC3B,YAAY,EAAE,SAAS;MACvB,UAAU,EAAE,SAAS;MACrB,aAAa,EAAE,WAAW;MAC1B,gBAAgB,EAAE,UAAU;MAC5B,cAAc,EAAE,MAAM;MACtB,oBAAoB,EAAE,YAAY;MAClC,eAAe,EAAE,QAAQ;MACzB,mBAAmB,EAAE,cAAc;MACnC,aAAa,EAAE,QAAQ;MACvB,cAAc,EAAE,qBAAqB;MACrC,eAAe,EAAE;IACnB;EACF;AACF,CAAC,CAAC;AAIF;AACA,OAAO,MAAM,gBAAgB,GAAG,MAA6B;EAC3D,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;EAC1B,OAAO,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAED,OAAO,MAAM,kBAAkB,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,338 @@
1
+ import { text } from "@k2b/stdlib";
2
+ import { checkMimeType } from "@k2b/stdlib/browser";
3
+ import { createSignal } from "solid-js";
4
+ import { announce } from "../feedback/announce";
5
+ import { toast } from "../feedback/toast";
6
+ /** The marker a hovered specific target carries for its ring; regions show their own overlay instead. */
7
+ export const FILE_DROP_ATTRIBUTE = "data-file-drop";
8
+
9
+ /** Where a region without `for` looks for its area: the nearest dialog, workspace detail, or workspace main area. */
10
+ export const FILE_DROP_DEFAULT_REGION = "dialog, .k2b-app-workspace__detail, .k2b-app-workspace__main";
11
+
12
+ /**
13
+ * The type every drag that starts in a page carries. Such a drag holds page content, never files from outside, even
14
+ * an image that carries one. Marking the drag itself leaves nothing behind when it never reports its end, as when its
15
+ * start is cancelled or its source is removed.
16
+ */
17
+ const PAGE_DRAG_TYPE = "application/x-k2b-page-drag";
18
+
19
+ /** How long after the last drag event a pointer move means the drag ended; a drag over the page repeats them sooner. */
20
+ const DRAG_SILENCE_MS = 600;
21
+ const engines = new WeakMap();
22
+ const isFileDrag = event => {
23
+ const types = Array.from(event.dataTransfer?.types ?? []);
24
+ return types.includes("Files") && !types.includes(PAGE_DRAG_TYPE);
25
+ };
26
+ const elementOf = target => target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
27
+ const isModal = dialog => {
28
+ try {
29
+ return dialog.matches(":modal");
30
+ } catch {
31
+ // An engine without `:modal` has no modal dialogs to defer to.
32
+ return false;
33
+ }
34
+ };
35
+
36
+ /**
37
+ * The topmost modal dialog. Everything outside of it is inert, so a drag over the page lands inside it; only a drag
38
+ * over other top-layer content falls back to the last modal dialog in the document.
39
+ */
40
+ const topModal = (doc, target) => {
41
+ for (let dialog = elementOf(target)?.closest("dialog"); dialog; dialog = dialog.parentElement?.closest("dialog")) if (isModal(dialog)) return dialog;
42
+ return Array.from(doc.querySelectorAll("dialog[open]")).filter(isModal).at(-1) ?? null;
43
+ };
44
+
45
+ /** Sorts dropped files into the ones a target takes and the ones it leaves out, in drop order. */
46
+ export const partitionDroppedFiles = (files, options) => {
47
+ const limit = options.multiple === false ? 1 : options.maxFiles ?? Number.POSITIVE_INFINITY;
48
+ const accepted = [];
49
+ const rejected = [];
50
+ for (const file of files) {
51
+ if (options.accept && !checkMimeType(file, options.accept)) rejected.push({
52
+ file,
53
+ reason: "type"
54
+ });else if (options.maxSize !== undefined && file.size > options.maxSize) rejected.push({
55
+ file,
56
+ reason: "size"
57
+ });else if (accepted.length >= limit) rejected.push({
58
+ file,
59
+ reason: "count"
60
+ });else accepted.push(file);
61
+ }
62
+ return {
63
+ accepted,
64
+ rejected
65
+ };
66
+ };
67
+
68
+ /**
69
+ * During a drag only the types are readable, and only in some engines. A drag is refused when none of them can fit.
70
+ * An extension in `accept` matches by file name, and systems report differing types for one extension (a `.csv` may be
71
+ * `application/vnd.ms-excel`), so with one the drop decides.
72
+ */
73
+ export const refusesDraggedTypes = (types, accept) => {
74
+ if (!accept || accept.split(",").some(entry => entry.trim().startsWith("."))) return false;
75
+ return types.length > 0 && types.every(type => type !== "" && !checkMimeType(type, accept));
76
+ };
77
+ const invalidDrag = (event, options) => refusesDraggedTypes(Array.from(event.dataTransfer?.items ?? []).filter(item => item.kind === "file").map(item => item.type), options.accept);
78
+ const MAX_NAMES = 3;
79
+ const nameList = (files, messages) => {
80
+ const names = files.slice(0, MAX_NAMES).map(file => file.name);
81
+ return files.length > MAX_NAMES ? `${names.join(", ")} ${messages.fileDropMore({
82
+ count: files.length - MAX_NAMES
83
+ })}` : names.join(", ");
84
+ };
85
+ export const fileDropRejectionMessage = (rejected, options, messages, locale) => {
86
+ const lines = [];
87
+ const of = reason => rejected.filter(entry => entry.reason === reason).map(entry => entry.file);
88
+ const type = of("type");
89
+ const size = of("size");
90
+ const count = of("count");
91
+ if (type.length) lines.push(messages.fileDropRejectedType({
92
+ names: nameList(type, messages)
93
+ }));
94
+ if (size.length) lines.push(messages.fileDropRejectedSize({
95
+ names: nameList(size, messages),
96
+ limit: text.pprintBytes(options.maxSize ?? 0, {
97
+ locale
98
+ })
99
+ }));
100
+ if (count.length) lines.push(messages.fileDropRejectedCount({
101
+ names: nameList(count, messages),
102
+ max: options.multiple === false ? 1 : options.maxFiles ?? 1
103
+ }));
104
+ return lines.join("\n");
105
+ };
106
+ const createEngine = doc => {
107
+ const view = doc.defaultView;
108
+ const regions = new Set();
109
+ const targets = new WeakMap();
110
+ const [session, setSession] = createSignal(null);
111
+ const [frame, setFrame] = createSignal(0);
112
+ /** Elements the drag has entered and not yet left; the drag is over the window while any remains. */
113
+ const entered = new Set();
114
+ let lastDragEvent = 0;
115
+ let marked = null;
116
+ let frameRequest = 0;
117
+ const mark = hover => {
118
+ if (marked && marked !== hover?.element) marked.removeAttribute(FILE_DROP_ATTRIBUTE);
119
+ marked = null;
120
+ if (!hover || hover.entry.region) return;
121
+ marked = hover.element;
122
+ marked.setAttribute(FILE_DROP_ATTRIBUTE, hover.invalid ? "invalid" : "over");
123
+ };
124
+ const regionElement = entry => {
125
+ const element = entry.region?.();
126
+ return element?.isConnected ? element : undefined;
127
+ };
128
+
129
+ /** Whether nothing between the target's host and its area hides it, as a dialog stack hides its lower levels. */
130
+ const displayed = (host, area) => {
131
+ for (let element = host; element && element !== area; element = element.parentElement) if (view?.getComputedStyle(element).display === "none") return false;
132
+ return true;
133
+ };
134
+
135
+ /** A region takes part when it is enabled, displayed, and not behind a modal dialog it is not inside of. */
136
+ const eligible = (entry, modal) => {
137
+ const element = regionElement(entry);
138
+ if (!element || entry.options.disabled) return false;
139
+ return (!modal || modal.contains(element)) && displayed(entry.host?.(), element);
140
+ };
141
+ const rendered = element => element === doc.documentElement || element.getClientRects().length > 0;
142
+ const begin = target => {
143
+ const modal = topModal(doc, target);
144
+ const candidates = [...regions].filter(entry => eligible(entry, modal) && rendered(regionElement(entry)));
145
+ // The latest region on an element replaces earlier ones; a region inside another shown one acts as a specific target.
146
+ const byElement = new Map();
147
+ for (const entry of candidates) byElement.set(regionElement(entry), entry);
148
+ const shown = [...byElement].filter(([element]) => ![...byElement.keys()].some(other => other !== element && other.contains(element)));
149
+ setSession({
150
+ shown: shown.map(([, entry]) => entry),
151
+ hover: null
152
+ });
153
+ const first = shown[0]?.[1];
154
+ if (first) announce(first.options.label);
155
+ };
156
+ const end = () => {
157
+ entered.clear();
158
+ mark(null);
159
+ if (session()) setSession(null);
160
+ };
161
+ const resolve = target => {
162
+ const modal = topModal(doc, target);
163
+ for (let element = elementOf(target); element; element = element.parentElement) {
164
+ const specific = targets.get(element);
165
+ if (specific && !specific.options.disabled) return {
166
+ entry: specific,
167
+ element
168
+ };
169
+ let region;
170
+ for (const entry of regions) if (regionElement(entry) === element && eligible(entry, modal)) region = entry;
171
+ if (region) return {
172
+ entry: region,
173
+ element
174
+ };
175
+ }
176
+ return null;
177
+ };
178
+ const hoverFrom = event => {
179
+ const current = session();
180
+ if (!current) return null;
181
+ const found = resolve(event.target);
182
+ const hover = found ? {
183
+ ...found,
184
+ invalid: invalidDrag(event, found.entry.options)
185
+ } : null;
186
+ const previous = current.hover;
187
+ if (previous?.entry !== hover?.entry || previous?.element !== hover?.element || previous?.invalid !== hover?.invalid) {
188
+ mark(hover);
189
+ setSession({
190
+ ...current,
191
+ hover
192
+ });
193
+ }
194
+ return hover;
195
+ };
196
+
197
+ /** Over a page with a region, a file never falls through to the browser, which would open it and leave the page. */
198
+ const guarding = () => [...regions].some(entry => regionElement(entry));
199
+ const accept = (event, hover) => {
200
+ if (!hover && !guarding()) return;
201
+ event.preventDefault();
202
+ if (event.dataTransfer) event.dataTransfer.dropEffect = hover && !hover.invalid ? "copy" : "none";
203
+ };
204
+ const requestFrame = () => {
205
+ if (frameRequest || !view) return;
206
+ frameRequest = view.requestAnimationFrame(() => {
207
+ frameRequest = 0;
208
+ setFrame(value => value + 1);
209
+ });
210
+ };
211
+ const deliver = (entry, dataTransfer) => {
212
+ const {
213
+ options
214
+ } = entry;
215
+ const files = Array.from(dataTransfer.files);
216
+ const {
217
+ accepted,
218
+ rejected
219
+ } = partitionDroppedFiles(files, options);
220
+ const messages = entry.messages();
221
+ if (rejected.length) toast.error(fileDropRejectionMessage(rejected, options, messages, entry.locale()));
222
+ if (!accepted.length && files.length) return;
223
+ if (accepted.length) announce(accepted.length === 1 ? messages.fileDropDroppedOne({
224
+ name: accepted[0].name
225
+ }) : messages.fileDropDropped({
226
+ count: accepted.length
227
+ }));
228
+ void options.onDrop(accepted, {
229
+ dataTransfer,
230
+ rejected
231
+ });
232
+ };
233
+ const listeners = {
234
+ dragstart: event => {
235
+ event.dataTransfer?.setData(PAGE_DRAG_TYPE, "");
236
+ },
237
+ dragenter: event => {
238
+ const drag = event;
239
+ if (!isFileDrag(drag)) return;
240
+ lastDragEvent = event.timeStamp;
241
+ if (event.target) entered.add(event.target);
242
+ if (!session()) begin(event.target);
243
+ accept(drag, hoverFrom(drag));
244
+ },
245
+ dragover: event => {
246
+ const drag = event;
247
+ if (!isFileDrag(drag)) return;
248
+ lastDragEvent = event.timeStamp;
249
+ // An engine may skip the first dragenter; the drag still counts as over the window.
250
+ if (!session()) {
251
+ if (event.target) entered.add(event.target);
252
+ begin(event.target);
253
+ }
254
+ accept(drag, hoverFrom(drag));
255
+ requestFrame();
256
+ },
257
+ dragleave: event => {
258
+ if (!session()) return;
259
+ if (event.target) entered.delete(event.target);
260
+ // An element removed during the drag never reports leaving.
261
+ for (const target of entered) if (target instanceof Node && !target.isConnected) entered.delete(target);
262
+ if (entered.size === 0) end();
263
+ },
264
+ drop: event => {
265
+ const drag = event;
266
+ if (!isFileDrag(drag)) return;
267
+ const found = resolve(event.target);
268
+ end();
269
+ if (!found) {
270
+ if (guarding()) event.preventDefault();
271
+ return;
272
+ }
273
+ // The most specific target owns the drop; editors and other handlers below it never see the files.
274
+ event.preventDefault();
275
+ event.stopPropagation();
276
+ if (drag.dataTransfer) deliver(found.entry, drag.dataTransfer);
277
+ },
278
+ keydown: event => {
279
+ if (event.key === "Escape" && session()) end();
280
+ },
281
+ // Engines send no pointer moves during a drag: one long after the last drag event means the drag ended somewhere the
282
+ // page did not see. One right after it belongs to another pointer, such as a second finger on a touch screen.
283
+ pointermove: event => {
284
+ if (session() && event.timeStamp - lastDragEvent > DRAG_SILENCE_MS) end();
285
+ },
286
+ visibilitychange: () => {
287
+ if (doc.visibilityState === "hidden") end();
288
+ }
289
+ };
290
+ let installed = false;
291
+ const install = () => {
292
+ if (installed || !view) return;
293
+ installed = true;
294
+ for (const [type, listener] of Object.entries(listeners)) (type === "visibilitychange" ? doc : view).addEventListener(type, listener, {
295
+ capture: true
296
+ });
297
+ };
298
+ return {
299
+ session,
300
+ frame,
301
+ addRegion: entry => {
302
+ install();
303
+ regions.add(entry);
304
+ return () => {
305
+ regions.delete(entry);
306
+ const current = session();
307
+ if (current?.hover?.entry === entry) {
308
+ mark(null);
309
+ setSession({
310
+ ...current,
311
+ hover: null
312
+ });
313
+ }
314
+ };
315
+ },
316
+ setTarget: (element, entry) => {
317
+ install();
318
+ targets.set(element, entry);
319
+ }
320
+ };
321
+ };
322
+
323
+ /**
324
+ * The document an element lives in. A freshly rendered element still belongs to the inert document of the template it
325
+ * was cloned from; until it is inserted, that is the page's document.
326
+ */
327
+ export const liveDocument = element => element.ownerDocument.defaultView ? element.ownerDocument : document;
328
+
329
+ /** The one drag tracker of a document, shared by every drop target in it. */
330
+ export const fileDropEngine = doc => {
331
+ let engine = engines.get(doc);
332
+ if (!engine) {
333
+ engine = createEngine(doc);
334
+ engines.set(doc, engine);
335
+ }
336
+ return engine;
337
+ };
338
+ //# sourceMappingURL=file-drop.js.map