@knowark/componarkjs 1.13.4 → 1.14.1

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 (118) hide show
  1. package/README.md +57 -45
  2. package/lib/base/component/component.js +142 -20
  3. package/lib/base/component/component.test.js +753 -374
  4. package/lib/base/component/index.js +3 -0
  5. package/lib/base/styles/index.js +4 -1
  6. package/lib/base/utils/define.js +30 -7
  7. package/lib/base/utils/define.test.js +129 -42
  8. package/lib/base/utils/format.js +12 -6
  9. package/lib/base/utils/format.test.js +16 -16
  10. package/lib/base/utils/helpers.js +42 -9
  11. package/lib/base/utils/helpers.test.js +134 -115
  12. package/lib/base/utils/index.js +1 -0
  13. package/lib/base/utils/slots.js +3 -2
  14. package/lib/base/utils/slots.test.js +38 -38
  15. package/lib/base/utils/uuid.js +1 -1
  16. package/lib/base/utils/uuid.test.js +13 -13
  17. package/lib/components/audio/components/audio.js +36 -3
  18. package/lib/components/audio/components/audio.test.js +120 -90
  19. package/lib/components/audio/index.js +1 -0
  20. package/lib/components/audio/styles/index.js +5 -1
  21. package/lib/components/camera/components/camera.js +15 -0
  22. package/lib/components/camera/components/camera.test.js +96 -91
  23. package/lib/components/camera/index.js +1 -0
  24. package/lib/components/camera/styles/index.js +5 -1
  25. package/lib/components/capture/components/capture.js +48 -4
  26. package/lib/components/capture/components/capture.test.js +165 -97
  27. package/lib/components/capture/index.js +1 -0
  28. package/lib/components/droparea/components/droparea-preview.js +114 -19
  29. package/lib/components/droparea/components/droparea-preview.test.js +344 -80
  30. package/lib/components/droparea/components/droparea.js +82 -6
  31. package/lib/components/droparea/components/droparea.test.js +309 -299
  32. package/lib/components/droparea/index.js +1 -0
  33. package/lib/components/droparea/styles/index.js +5 -1
  34. package/lib/components/emit/components/emit.js +34 -4
  35. package/lib/components/emit/components/emit.test.js +192 -134
  36. package/lib/components/emit/index.js +1 -0
  37. package/lib/components/index.js +2 -1
  38. package/lib/components/list/components/item.js +6 -0
  39. package/lib/components/list/components/item.test.js +69 -68
  40. package/lib/components/list/components/list.js +51 -7
  41. package/lib/components/list/components/list.test.js +358 -227
  42. package/lib/components/list/index.js +1 -0
  43. package/lib/components/paginator/components/paginator.js +34 -8
  44. package/lib/components/paginator/components/paginator.test.js +146 -143
  45. package/lib/components/paginator/index.js +1 -0
  46. package/lib/components/paginator/styles/index.js +5 -1
  47. package/lib/components/spinner/components/spinner.js +10 -0
  48. package/lib/components/spinner/components/spinner.test.js +36 -41
  49. package/lib/components/spinner/index.js +1 -0
  50. package/lib/components/spinner/styles/index.js +5 -1
  51. package/lib/components/splitview/components/splitview.detail.js +10 -1
  52. package/lib/components/splitview/components/splitview.detail.test.js +75 -73
  53. package/lib/components/splitview/components/splitview.js +54 -11
  54. package/lib/components/splitview/components/splitview.master.js +37 -2
  55. package/lib/components/splitview/components/splitview.master.test.js +52 -52
  56. package/lib/components/splitview/components/splitview.test.js +135 -31
  57. package/lib/components/splitview/index.js +1 -0
  58. package/lib/components/translate/components/translate.js +65 -14
  59. package/lib/components/translate/components/translate.test.js +658 -131
  60. package/lib/components/translate/index.js +1 -0
  61. package/lib/index.js +3 -0
  62. package/package.json +4 -27
  63. package/scripts/node-test-setup.js +94 -0
  64. package/tsconfig.json +1 -1
  65. package/types/base/component/component.d.ts +43 -8
  66. package/types/base/component/component.d.ts.map +1 -1
  67. package/types/base/component/index.d.ts +4 -6
  68. package/types/base/component/index.d.ts.map +1 -1
  69. package/types/base/styles/index.d.ts +3 -2
  70. package/types/base/styles/index.d.ts.map +1 -1
  71. package/types/base/utils/define.d.ts +3 -2
  72. package/types/base/utils/define.d.ts.map +1 -1
  73. package/types/base/utils/format.d.ts +12 -6
  74. package/types/base/utils/format.d.ts.map +1 -1
  75. package/types/base/utils/helpers.d.ts +27 -7
  76. package/types/base/utils/helpers.d.ts.map +1 -1
  77. package/types/base/utils/slots.d.ts +8 -10
  78. package/types/base/utils/slots.d.ts.map +1 -1
  79. package/types/base/utils/uuid.d.ts +1 -1
  80. package/types/base/utils/uuid.d.ts.map +1 -1
  81. package/types/components/audio/components/audio.d.ts +23 -9
  82. package/types/components/audio/components/audio.d.ts.map +1 -1
  83. package/types/components/audio/styles/index.d.ts +3 -2
  84. package/types/components/audio/styles/index.d.ts.map +1 -1
  85. package/types/components/camera/components/camera.d.ts +11 -3
  86. package/types/components/camera/components/camera.d.ts.map +1 -1
  87. package/types/components/camera/styles/index.d.ts +3 -2
  88. package/types/components/camera/styles/index.d.ts.map +1 -1
  89. package/types/components/capture/components/capture.d.ts +23 -3
  90. package/types/components/capture/components/capture.d.ts.map +1 -1
  91. package/types/components/droparea/components/droparea-preview.d.ts +64 -11
  92. package/types/components/droparea/components/droparea-preview.d.ts.map +1 -1
  93. package/types/components/droparea/components/droparea.d.ts +58 -13
  94. package/types/components/droparea/components/droparea.d.ts.map +1 -1
  95. package/types/components/droparea/styles/index.d.ts +3 -2
  96. package/types/components/droparea/styles/index.d.ts.map +1 -1
  97. package/types/components/emit/components/emit.d.ts +15 -3
  98. package/types/components/emit/components/emit.d.ts.map +1 -1
  99. package/types/components/list/components/item.d.ts +8 -1
  100. package/types/components/list/components/item.d.ts.map +1 -1
  101. package/types/components/list/components/list.d.ts +23 -5
  102. package/types/components/list/components/list.d.ts.map +1 -1
  103. package/types/components/paginator/components/paginator.d.ts +32 -8
  104. package/types/components/paginator/components/paginator.d.ts.map +1 -1
  105. package/types/components/paginator/styles/index.d.ts +3 -2
  106. package/types/components/paginator/styles/index.d.ts.map +1 -1
  107. package/types/components/spinner/components/spinner.d.ts +14 -3
  108. package/types/components/spinner/components/spinner.d.ts.map +1 -1
  109. package/types/components/spinner/styles/index.d.ts +3 -2
  110. package/types/components/spinner/styles/index.d.ts.map +1 -1
  111. package/types/components/splitview/components/splitview.d.ts +22 -4
  112. package/types/components/splitview/components/splitview.d.ts.map +1 -1
  113. package/types/components/splitview/components/splitview.detail.d.ts +12 -2
  114. package/types/components/splitview/components/splitview.detail.d.ts.map +1 -1
  115. package/types/components/splitview/components/splitview.master.d.ts +12 -1
  116. package/types/components/splitview/components/splitview.master.d.ts.map +1 -1
  117. package/types/components/translate/components/translate.d.ts +44 -10
  118. package/types/components/translate/components/translate.d.ts.map +1 -1
@@ -1,107 +1,371 @@
1
- import { jest } from '@jest/globals'
1
+ import { it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
  import './droparea-preview.js'
3
4
 
4
- describe('Droparea', () => {
5
- const createBubbledEvent = (type, props = {}) => {
6
- const event = new Event(type, {
7
- bubbles: true
8
- })
9
- Object.assign(event, props)
10
- return event
11
- }
5
+ const createBubbledEvent = (type, props = {}) => {
6
+ const event = new Event(type, {
7
+ bubbles: true
8
+ })
9
+ Object.assign(event, props)
10
+ return event
11
+ }
12
12
 
13
- global.URL.createObjectURL = (
14
- /** @type {(obj: Blob | MediaSource) => string} */ (jest.fn()))
15
- global.document.elementFromPoint = (
16
- /** @type {(x: number, y: number) => Element} */ (jest.fn()))
13
+ let objectURLCount = 0
14
+ global.URL.createObjectURL = (
15
+ /** @type {(obj: Blob | MediaSource) => string} */ (
16
+ mock.fn(() => `mock://data/url/${objectURLCount++}`)))
17
+ global.URL.revokeObjectURL = (
18
+ /** @type {(url: string) => void} */ (mock.fn()))
19
+ global.document.elementFromPoint = (
20
+ /** @type {(x: number, y: number) => Element} */ (mock.fn()))
17
21
 
18
- let container = null
22
+ let container = null
19
23
 
20
- beforeEach(() => {
21
- container = document.createElement('div')
22
- document.body.appendChild(container)
23
- })
24
+ const setup = () => {
25
+ document.body.innerHTML = ''
26
+ objectURLCount = 0
27
+ global.URL.createObjectURL.mock.resetCalls()
28
+ global.URL.revokeObjectURL.mock.resetCalls()
29
+ container = document.createElement('div')
30
+ document.body.appendChild(container)
31
+ }
32
+
33
+ it('can be instantiated', () => {
34
+ setup()
35
+ container.innerHTML = /* html */ `
36
+ <ark-droparea></ark-droparea>
37
+ `
38
+ const droparea = container.querySelector('ark-droparea')
39
+ const preview = droparea.querySelector('ark-droparea-preview')
40
+ assert.strictEqual(preview, preview.init())
41
+ })
24
42
 
25
- afterEach(() => {
26
- container.remove()
27
- container = null
43
+ it('Item can be removed', () => {
44
+ setup()
45
+ container.innerHTML = /* html */ `
46
+ <ark-droparea></ark-droparea>
47
+ `
48
+
49
+ const droparea = container.querySelector('ark-droparea')
50
+ const preview = droparea.querySelector('[data-preview-list]')
51
+ const dropZone = droparea.querySelector('.ark-droparea__form')
52
+ const myFile = new File(['image'], 'Doggy.png', {
53
+ type: 'image/png'
54
+ })
55
+ const myFile2 = new File(['image'], 'Scooby.png', {
56
+ type: 'image/png'
57
+ })
58
+ const dropEvent = createBubbledEvent('drop', {
59
+ clientX: 0,
60
+ clientY: 1,
61
+ dataTransfer: {
62
+ files: [myFile, myFile2]
63
+ }
28
64
  })
29
65
 
30
- it('can be instantiated', () => {
31
- container.innerHTML = /* html */ `
32
- <ark-droparea></ark-droparea>
33
- `
34
- const droparea = container.querySelector('ark-droparea')
35
- const preview = droparea.querySelector('ark-droparea-preview')
36
- expect(preview).toBe(preview.init())
66
+ dropZone.dispatchEvent(dropEvent)
67
+ preview.querySelector('button').click()
68
+ preview.querySelector('button').click()
69
+ })
70
+
71
+ it('Can drag previews and sort a new list', () => {
72
+ setup()
73
+ container.innerHTML = /* html */ `
74
+ <ark-droparea></ark-droparea>
75
+ `
76
+
77
+ const droparea = container.querySelector('ark-droparea')
78
+ const preview = droparea.querySelector('[data-preview-list]')
79
+ const dropZone = droparea.querySelector('.ark-droparea__form')
80
+ const myFile = new File(['image'], 'Doggy.png', {
81
+ type: 'image/png'
82
+ })
83
+ const myFile2 = new File(['image'], 'Scooby.png', {
84
+ type: 'image/png'
85
+ })
86
+ const dropEvent = createBubbledEvent('drop', {
87
+ clientX: 0,
88
+ clientY: 1,
89
+ dataTransfer: {
90
+ files: [myFile, myFile2]
91
+ }
37
92
  })
38
93
 
39
- it('Item can be removed', () => {
40
- container.innerHTML = /* html */ `
41
- <ark-droparea></ark-droparea>
42
- `
94
+ dropZone.dispatchEvent(dropEvent)
95
+
96
+ preview.handleDrag = mock.fn()
97
+
98
+ const getThumbnails = () =>
99
+ Array.from(preview.querySelectorAll('.ark-droparea-preview__frame'))
100
+
101
+ const thumbnails = getThumbnails()
102
+
103
+ const startingNode = thumbnails[0]
104
+ const endingNode = thumbnails[1]
105
+
106
+ startingNode.dispatchEvent(
107
+ createBubbledEvent('dragstart', { clientX: 0, clientY: 0 })
108
+ )
109
+ endingNode.dispatchEvent(
110
+ createBubbledEvent('dragend', { clientX: 0, clientY: 1 })
111
+ )
112
+ })
43
113
 
114
+ it('handleDrag keeps the selected item when elementFromPoint returns null', () => {
115
+ setup()
116
+ const preview = document.createElement('ark-droparea-preview')
117
+ const list = document.createElement('ul')
118
+ const firstItem = document.createElement('li')
119
+ const secondItem = document.createElement('li')
120
+ list.append(firstItem, secondItem)
121
+ preview.appendChild(list)
122
+ document.body.appendChild(preview)
123
+
124
+ const originalElementFromPoint = document.elementFromPoint
125
+ document.elementFromPoint = () => null
126
+
127
+ preview.handleDrag(firstItem, { clientX: 0, clientY: 0 })
128
+
129
+ assert.ok(firstItem.classList.contains('drag-sort-active'))
130
+ assert.strictEqual(list.firstElementChild, firstItem)
131
+
132
+ document.elementFromPoint = originalElementFromPoint
133
+ })
134
+
135
+ it('handleDrag moves the selected item after its next sibling', () => {
136
+ setup()
137
+ const preview = document.createElement('ark-droparea-preview')
138
+ const list = document.createElement('ul')
139
+ const firstItem = document.createElement('li')
140
+ const secondItem = document.createElement('li')
141
+ const thirdItem = document.createElement('li')
142
+ list.append(firstItem, secondItem, thirdItem)
143
+ preview.appendChild(list)
144
+ document.body.appendChild(preview)
145
+
146
+ const originalElementFromPoint = document.elementFromPoint
147
+ document.elementFromPoint = () => secondItem
148
+
149
+ preview.handleDrag(firstItem, { clientX: 1, clientY: 1 })
150
+
151
+ assert.strictEqual(list.children[0], secondItem)
152
+ assert.strictEqual(list.children[1], firstItem)
153
+ assert.strictEqual(list.children[2], thirdItem)
154
+
155
+ document.elementFromPoint = originalElementFromPoint
156
+ })
157
+
158
+ it('does not add duplicate drag listeners to existing previews', () => {
159
+ setup()
160
+ const counts = new WeakMap()
161
+ const originalAddEventListener = Element.prototype.addEventListener
162
+ Element.prototype.addEventListener = function (type, listener, options) {
163
+ const isDragEvent = type === 'drag' || type === 'dragend'
164
+ if (this.tagName === 'LI' && isDragEvent) {
165
+ const itemCounts = counts.get(this) || { drag: 0, dragend: 0 }
166
+ itemCounts[type] += 1
167
+ counts.set(this, itemCounts)
168
+ }
169
+
170
+ return originalAddEventListener.call(this, type, listener, options)
171
+ }
172
+
173
+ try {
174
+ container.innerHTML = /* html */ `
175
+ <ark-droparea></ark-droparea>
176
+ `
44
177
  const droparea = container.querySelector('ark-droparea')
45
- const preview = droparea.querySelector('[data-preview-list]')
46
178
  const dropZone = droparea.querySelector('.ark-droparea__form')
47
- const myFile = new File(['image'], 'Doggy.png', {
48
- type: 'image/png'
49
- })
50
- const myFile2 = new File(['image'], 'Scooby.png', {
51
- type: 'image/png'
179
+
180
+ const firstBatch = createBubbledEvent('drop', {
181
+ dataTransfer: { files: [new File(['image'], 'Doggy.png', { type: 'image/png' })] }
52
182
  })
53
- const dropEvent = createBubbledEvent('drop', {
54
- clientX: 0,
55
- clientY: 1,
56
- dataTransfer: {
57
- files: [myFile, myFile2]
58
- }
183
+ const secondBatch = createBubbledEvent('drop', {
184
+ dataTransfer: { files: [new File(['image'], 'Scooby.png', { type: 'image/png' })] }
59
185
  })
60
186
 
61
- dropZone.dispatchEvent(dropEvent)
62
- preview.querySelector('button').click()
63
- preview.querySelector('button').click()
187
+ dropZone.dispatchEvent(firstBatch)
188
+ dropZone.dispatchEvent(secondBatch)
189
+
190
+ const firstItem = droparea.querySelector('.ark-droparea-preview__frame')
191
+ const itemCounts = counts.get(firstItem)
192
+
193
+ assert.deepStrictEqual(itemCounts.drag, 1)
194
+ assert.deepStrictEqual(itemCounts.dragend, 1)
195
+ } finally {
196
+ Element.prototype.addEventListener = originalAddEventListener
197
+ }
198
+ })
199
+
200
+ it('reuses object URLs for the media list and revokes on file removal', () => {
201
+ setup()
202
+ container.innerHTML = /* html */ `
203
+ <ark-droparea></ark-droparea>
204
+ `
205
+ const droparea = container.querySelector('ark-droparea')
206
+ const dropZone = droparea.querySelector('.ark-droparea__form')
207
+ const myFile = new File(['image'], 'Doggy.png', {
208
+ type: 'image/png'
64
209
  })
65
210
 
66
- it('Can drag previews and sort a new list', () => {
67
- container.innerHTML = /* html */ `
68
- <ark-droparea></ark-droparea>
69
- `
211
+ dropZone.dispatchEvent(createBubbledEvent('drop', {
212
+ dataTransfer: { files: [myFile] }
213
+ }))
70
214
 
71
- const droparea = container.querySelector('ark-droparea')
72
- const preview = droparea.querySelector('[data-preview-list]')
73
- const dropZone = droparea.querySelector('.ark-droparea__form')
74
- const myFile = new File(['image'], 'Doggy.png', {
75
- type: 'image/png'
76
- })
77
- const myFile2 = new File(['image'], 'Scooby.png', {
78
- type: 'image/png'
79
- })
80
- const dropEvent = createBubbledEvent('drop', {
81
- clientX: 0,
82
- clientY: 1,
83
- dataTransfer: {
84
- files: [myFile, myFile2]
85
- }
86
- })
215
+ const callsBeforeMediaRead = global.URL.createObjectURL.mock.calls.length
216
+ const mediaUrl = droparea.mediaList[0].url
217
+ const mediaUrlSecondRead = droparea.mediaList[0].url
218
+
219
+ assert.deepStrictEqual(mediaUrl, mediaUrlSecondRead)
220
+ assert.deepStrictEqual(
221
+ global.URL.createObjectURL.mock.calls.length,
222
+ callsBeforeMediaRead
223
+ )
87
224
 
88
- dropZone.dispatchEvent(dropEvent)
225
+ droparea.querySelector('.ark-droparea__remove').click()
226
+ assert.deepStrictEqual(
227
+ global.URL.revokeObjectURL.mock.calls[0].arguments[0],
228
+ mediaUrl
229
+ )
230
+ })
89
231
 
90
- preview.handleDrag = jest.fn()
232
+ it('returns early when handleDrop runs outside a droparea component', () => {
233
+ setup()
234
+ const preview = document.createElement('ark-droparea-preview')
235
+ const target = document.createElement('li')
236
+ preview.appendChild(target)
237
+ document.body.appendChild(preview)
91
238
 
92
- const getThumbnails = () =>
93
- Array.from(preview.querySelectorAll('.ark-droparea-preview__frame'))
239
+ assert.doesNotThrow(() => {
240
+ preview.handleDrop({ target })
241
+ })
242
+ })
94
243
 
95
- const thumbnails = getThumbnails()
244
+ it('returns early when removing a file that does not exist', () => {
245
+ setup()
246
+ container.innerHTML = /* html */ `
247
+ <ark-droparea></ark-droparea>
248
+ `
249
+ const droparea = container.querySelector('ark-droparea')
250
+ const preview = droparea.preview
251
+ const button = document.createElement('button')
252
+ const frame = document.createElement('li')
253
+ frame.appendChild(button)
254
+ preview.select('[data-preview-list]').appendChild(frame)
96
255
 
97
- const startingNode = thumbnails[0]
98
- const endingNode = thumbnails[1]
256
+ const unknownFile = new File(['image'], 'unknown.png', { type: 'image/png' })
99
257
 
100
- startingNode.dispatchEvent(
101
- createBubbledEvent('dragstart', { clientX: 0, clientY: 0 })
102
- )
103
- endingNode.dispatchEvent(
104
- createBubbledEvent('dragend', { clientX: 0, clientY: 1 })
105
- )
258
+ assert.doesNotThrow(() => {
259
+ preview.removeFile(unknownFile, { target: button })
106
260
  })
261
+ assert.ok(preview.select('[data-preview-list]').contains(frame))
262
+ })
263
+
264
+ it('skips drag listeners for already-enabled items', () => {
265
+ setup()
266
+ const preview = document.createElement('ark-droparea-preview')
267
+ const item = document.createElement('li')
268
+ item.setAttribute('data-drag-enabled', '')
269
+ let calls = 0
270
+ const originalAddEventListener = item.addEventListener
271
+ item.addEventListener = function (...args) {
272
+ calls += 1
273
+ return originalAddEventListener.apply(this, args)
274
+ }
275
+
276
+ preview.enableDragItem(item)
277
+
278
+ assert.deepStrictEqual(calls, 0)
279
+ })
280
+
281
+ it('does nothing when revoking a file without an object URL', () => {
282
+ setup()
283
+ const preview = document.createElement('ark-droparea-preview')
284
+ const file = new File(['image'], 'Snoopy.png', { type: 'image/png' })
285
+
286
+ assert.doesNotThrow(() => {
287
+ preview.revokeFile(file)
288
+ })
289
+ })
290
+
291
+ it('renders a text preview in single mode and clears previous preview items', () => {
292
+ setup()
293
+ container.innerHTML = /* html */ `
294
+ <ark-droparea single></ark-droparea>
295
+ `
296
+ const droparea = container.querySelector('ark-droparea')
297
+ const dropZone = droparea.querySelector('.ark-droparea__form')
298
+ const firstFile = new File(['first'], 'first.txt', { type: 'text/plain' })
299
+ const secondFile = new File(['second'], 'second.txt', { type: 'text/plain' })
300
+
301
+ dropZone.dispatchEvent(createBubbledEvent('drop', {
302
+ dataTransfer: { files: [firstFile] }
303
+ }))
304
+
305
+ const firstPreviewItem = droparea.preview.querySelector('li')
306
+ assert.deepStrictEqual(firstPreviewItem.querySelector('p').textContent, 'first.txt')
307
+ assert.ok(firstPreviewItem.getAttribute('data').includes('mock://data/url/'))
308
+ assert.strictEqual(firstPreviewItem.getAttribute('index'), null)
309
+
310
+ dropZone.dispatchEvent(createBubbledEvent('drop', {
311
+ dataTransfer: { files: [secondFile] }
312
+ }))
313
+
314
+ const frames = droparea.preview.querySelectorAll('li')
315
+ assert.deepStrictEqual(frames.length, 1)
316
+ assert.deepStrictEqual(frames[0].querySelector('p').textContent, 'second.txt')
317
+ })
318
+
319
+ it('clearPreview tolerates missing preview list nodes', () => {
320
+ setup()
321
+ const preview = document.createElement('ark-droparea-preview')
322
+ const originalSelect = preview.select
323
+ preview.select = () => null
324
+ preview.toggleVisibility = () => {}
325
+
326
+ assert.doesNotThrow(() => {
327
+ preview.clearPreview()
328
+ })
329
+
330
+ preview.select = originalSelect
331
+ })
332
+
333
+ it('returns early when handleDrop receives a null target', () => {
334
+ setup()
335
+ const preview = document.createElement('ark-droparea-preview')
336
+
337
+ assert.doesNotThrow(() => {
338
+ preview.handleDrop({ target: null })
339
+ })
340
+ })
341
+
342
+ it('returns early when removeFile receives a null target', () => {
343
+ setup()
344
+ const preview = document.createElement('ark-droparea-preview')
345
+ const file = new File(['image'], 'Nully.png', { type: 'image/png' })
346
+
347
+ assert.doesNotThrow(() => {
348
+ preview.removeFile(file, { target: null })
349
+ })
350
+ })
351
+
352
+ it('revokeFile and revokeAllFiles tolerate missing revokeObjectURL', () => {
353
+ setup()
354
+ const preview = document.createElement('ark-droparea-preview')
355
+ const file = new File(['one'], 'one.txt', { type: 'text/plain' })
356
+ const secondFile = new File(['two'], 'two.txt', { type: 'text/plain' })
357
+ preview._objectUrls.set(file, 'mock://data/url/a')
358
+ preview._objectUrls.set(secondFile, 'mock://data/url/b')
359
+
360
+ const originalRevoke = global.URL.revokeObjectURL
361
+ global.URL.revokeObjectURL = undefined
362
+
363
+ try {
364
+ assert.doesNotThrow(() => {
365
+ preview.revokeFile(file)
366
+ preview.revokeAllFiles()
367
+ })
368
+ } finally {
369
+ global.URL.revokeObjectURL = originalRevoke
370
+ }
107
371
  })
@@ -5,8 +5,21 @@ import './droparea-preview.js'
5
5
  /** @import {DropareaPreview} from './droparea-preview.js' */
6
6
 
7
7
  const tag = 'ark-droparea'
8
+ /**
9
+ * Drag-and-drop and file input area.
10
+ * Emits:
11
+ * - `alter` with current media list details.
12
+ */
8
13
 
9
14
  export class Droparea extends Component {
15
+ constructor () {
16
+ super()
17
+ this._onChange = this.onChange.bind(this)
18
+ this._onOpenInput = this.openInput.bind(this)
19
+ }
20
+
21
+ /** @param {object} context
22
+ * @returns {this} */
10
23
  init (context = {}) {
11
24
  this.fileList = []
12
25
  this.contextFiles = context.contextFiles || this.contextFiles || []
@@ -16,6 +29,7 @@ export class Droparea extends Component {
16
29
  return super.init()
17
30
  }
18
31
 
32
+ /** @returns {this} */
19
33
  render () {
20
34
  this._grabSlots()
21
35
  this.content = /* html */ `
@@ -37,11 +51,15 @@ export class Droparea extends Component {
37
51
  return super.render()
38
52
  }
39
53
 
54
+ /** @returns {string[]} */
40
55
  reflectedProperties () {
41
56
  return ['size', 'accept', 'maxSize', 'title']
42
57
  }
43
58
 
59
+ /** @returns {Promise<void>} */
44
60
  async load () {
61
+ this._detachListeners()
62
+
45
63
  this.dragDropEvents.forEach((eventName) => {
46
64
  this.addEventListener(eventName, this.preventDefaults, false)
47
65
  })
@@ -55,8 +73,8 @@ export class Droparea extends Component {
55
73
  })
56
74
 
57
75
  this.addEventListener('drop', this.handleDrop, false)
58
- this._input.addEventListener('change', this.onChange.bind(this))
59
- this.openButton.addEventListener('click', this.openInput.bind(this))
76
+ this._input?.addEventListener('change', this._onChange)
77
+ this.openButton?.addEventListener('click', this._onOpenInput)
60
78
 
61
79
  /* istanbul ignore else */
62
80
  if (this.contextFiles) {
@@ -64,25 +82,66 @@ export class Droparea extends Component {
64
82
  }
65
83
  }
66
84
 
85
+ /** @returns {void} */
86
+ disconnectedCallback () {
87
+ this._detachListeners()
88
+ super.disconnectedCallback()
89
+ }
90
+
91
+ /** @returns {void} */
92
+ _detachListeners () {
93
+ if (this.dragDropEvents) {
94
+ this.dragDropEvents.forEach((eventName) => {
95
+ this.removeEventListener(eventName, this.preventDefaults, false)
96
+ })
97
+ }
98
+
99
+ if (this.dragEvents) {
100
+ this.dragEvents.forEach((eventName) => {
101
+ this.removeEventListener(eventName, this.highlight, false)
102
+ })
103
+ }
104
+
105
+ if (this.dropEvents) {
106
+ this.dropEvents.forEach((eventName) => {
107
+ this.removeEventListener(eventName, this.unhighlight, false)
108
+ })
109
+ }
110
+
111
+ this.removeEventListener('drop', this.handleDrop, false)
112
+ this._input?.removeEventListener('change', this._onChange)
113
+ this.openButton?.removeEventListener('click', this._onOpenInput)
114
+ }
115
+
116
+ /** @param {Event} event
117
+ * @returns {void} */
67
118
  openInput (event) {
68
119
  event.stopPropagation()
69
120
  const input = this.select('[data-input]')
70
121
  input.click()
71
122
  }
72
123
 
124
+ /** @param {Event} event
125
+ * @returns {void} */
73
126
  preventDefaults (event) {
74
127
  event.preventDefault()
75
128
  event.stopPropagation()
76
129
  }
77
130
 
131
+ /** @param {Event} event
132
+ * @returns {void} */
78
133
  highlight (event) {
79
134
  this.dropZone.classList.add('highlight')
80
135
  }
81
136
 
137
+ /** @param {Event} event
138
+ * @returns {void} */
82
139
  unhighlight (event) {
83
140
  this.dropZone.classList.remove('highlight')
84
141
  }
85
142
 
143
+ /** @param {DragEvent} event
144
+ * @returns {void} */
86
145
  handleDrop (event) {
87
146
  event.stopPropagation()
88
147
  const data = event.dataTransfer
@@ -90,13 +149,17 @@ export class Droparea extends Component {
90
149
  this.handleFiles(files)
91
150
  }
92
151
 
152
+ /** @param {Event} event
153
+ * @returns {void} */
93
154
  onChange (event) {
94
155
  event.stopPropagation()
95
- const input = event.target
156
+ const input = /** @type {HTMLInputElement} */ (event.target)
96
157
  const files = input.files
97
158
  this.handleFiles(files)
98
159
  }
99
160
 
161
+ /** @param {FileList|File[]} files
162
+ * @returns {void} */
100
163
  handleFiles (files) {
101
164
  if (this.single) {
102
165
  files = [files[0]]
@@ -106,8 +169,9 @@ export class Droparea extends Component {
106
169
  !this.preview.fileExists(files[0]) &&
107
170
  this.maxSizeValidate(files[0])
108
171
  ) {
172
+ this.fileList[0] && this.preview.revokeFile(this.fileList[0])
109
173
  this.fileList[0] = files[0]
110
- this.preview.querySelector('[data-preview-list]').innerHTML = ''
174
+ this.preview.clearPreview()
111
175
  this.preview.previewFile(files[0])
112
176
  }
113
177
  } else {
@@ -125,6 +189,10 @@ export class Droparea extends Component {
125
189
  this.preview.dispatchAlterEvent()
126
190
  }
127
191
 
192
+ /**
193
+ * @param {File[]|FileList} fileList
194
+ * @returns {boolean}
195
+ */
128
196
  validate (fileList) {
129
197
  if (!this.accept || this.accept.length === 0) return true
130
198
  const acceptList = this.accept.split(',').map(
@@ -148,16 +216,20 @@ export class Droparea extends Component {
148
216
  return true
149
217
  }
150
218
 
219
+ /** @param {File} file
220
+ * @returns {boolean} */
151
221
  maxSizeValidate (file) {
152
222
  return true
153
223
  }
154
224
 
225
+ /** @returns {void} */
155
226
  _grabSlots() {
156
227
  const [fileInput] = [this.slots.general].flat()
157
228
  this.fileInput = this.fileInput ?? fileInput
158
-
159
229
  }
160
230
 
231
+ /** @param {HTMLInputElement|undefined} element
232
+ * @returns {HTMLInputElement} */
161
233
  _buildFileInput (element) {
162
234
  const input = element ?? document.createElement('input')
163
235
  const attributes = [['class', `${tag}__input`],
@@ -166,14 +238,18 @@ export class Droparea extends Component {
166
238
  return input
167
239
  }
168
240
 
241
+ /** @returns {HTMLFormElement} */
169
242
  get dropZone () {
170
- return this.select('.ark-droparea__form')
243
+ return /** @type {HTMLFormElement} */ (
244
+ this.querySelector('.ark-droparea__form'))
171
245
  }
172
246
 
247
+ /** @returns {DropareaPreview} */
173
248
  get preview () {
174
249
  return /** @type {DropareaPreview} */ (this.select('ark-droparea-preview'))
175
250
  }
176
251
 
252
+ /** @returns {Array<{name:string,type:string,size:number,url:string}>} */
177
253
  get mediaList () {
178
254
  return this.preview.mediaList
179
255
  }