@knowark/componarkjs 1.14.0 → 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 (100) hide show
  1. package/README.md +57 -45
  2. package/lib/base/component/component.js +127 -21
  3. package/lib/base/component/component.test.js +296 -3
  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 +2 -1
  7. package/lib/base/utils/format.js +12 -6
  8. package/lib/base/utils/helpers.js +31 -5
  9. package/lib/base/utils/index.js +1 -0
  10. package/lib/base/utils/slots.js +3 -2
  11. package/lib/base/utils/uuid.js +1 -1
  12. package/lib/components/audio/components/audio.js +17 -2
  13. package/lib/components/audio/index.js +1 -0
  14. package/lib/components/audio/styles/index.js +5 -1
  15. package/lib/components/camera/components/camera.js +10 -0
  16. package/lib/components/camera/index.js +1 -0
  17. package/lib/components/camera/styles/index.js +5 -1
  18. package/lib/components/capture/components/capture.js +18 -2
  19. package/lib/components/capture/index.js +1 -0
  20. package/lib/components/droparea/components/droparea-preview.js +58 -13
  21. package/lib/components/droparea/components/droparea-preview.test.js +82 -0
  22. package/lib/components/droparea/components/droparea.js +41 -2
  23. package/lib/components/droparea/index.js +1 -0
  24. package/lib/components/droparea/styles/index.js +5 -1
  25. package/lib/components/emit/components/emit.js +11 -1
  26. package/lib/components/emit/index.js +1 -0
  27. package/lib/components/index.js +2 -1
  28. package/lib/components/list/components/item.js +6 -0
  29. package/lib/components/list/components/list.js +18 -4
  30. package/lib/components/list/index.js +1 -0
  31. package/lib/components/paginator/components/paginator.js +34 -8
  32. package/lib/components/paginator/index.js +1 -0
  33. package/lib/components/paginator/styles/index.js +5 -1
  34. package/lib/components/spinner/components/spinner.js +10 -0
  35. package/lib/components/spinner/index.js +1 -0
  36. package/lib/components/spinner/styles/index.js +5 -1
  37. package/lib/components/splitview/components/splitview.detail.js +10 -1
  38. package/lib/components/splitview/components/splitview.js +18 -3
  39. package/lib/components/splitview/components/splitview.master.js +10 -0
  40. package/lib/components/splitview/index.js +1 -0
  41. package/lib/components/translate/components/translate.js +42 -11
  42. package/lib/components/translate/components/translate.test.js +169 -1
  43. package/lib/components/translate/index.js +1 -0
  44. package/lib/index.js +3 -0
  45. package/package.json +2 -1
  46. package/tsconfig.json +1 -1
  47. package/types/base/component/component.d.ts +43 -8
  48. package/types/base/component/component.d.ts.map +1 -1
  49. package/types/base/component/index.d.ts +4 -6
  50. package/types/base/component/index.d.ts.map +1 -1
  51. package/types/base/styles/index.d.ts +3 -2
  52. package/types/base/styles/index.d.ts.map +1 -1
  53. package/types/base/utils/define.d.ts +3 -2
  54. package/types/base/utils/define.d.ts.map +1 -1
  55. package/types/base/utils/format.d.ts +12 -6
  56. package/types/base/utils/format.d.ts.map +1 -1
  57. package/types/base/utils/helpers.d.ts +27 -7
  58. package/types/base/utils/helpers.d.ts.map +1 -1
  59. package/types/base/utils/slots.d.ts +8 -10
  60. package/types/base/utils/slots.d.ts.map +1 -1
  61. package/types/base/utils/uuid.d.ts +1 -1
  62. package/types/base/utils/uuid.d.ts.map +1 -1
  63. package/types/components/audio/components/audio.d.ts +23 -9
  64. package/types/components/audio/components/audio.d.ts.map +1 -1
  65. package/types/components/audio/styles/index.d.ts +3 -2
  66. package/types/components/audio/styles/index.d.ts.map +1 -1
  67. package/types/components/camera/components/camera.d.ts +11 -3
  68. package/types/components/camera/components/camera.d.ts.map +1 -1
  69. package/types/components/camera/styles/index.d.ts +3 -2
  70. package/types/components/camera/styles/index.d.ts.map +1 -1
  71. package/types/components/capture/components/capture.d.ts +23 -3
  72. package/types/components/capture/components/capture.d.ts.map +1 -1
  73. package/types/components/droparea/components/droparea-preview.d.ts +64 -11
  74. package/types/components/droparea/components/droparea-preview.d.ts.map +1 -1
  75. package/types/components/droparea/components/droparea.d.ts +58 -13
  76. package/types/components/droparea/components/droparea.d.ts.map +1 -1
  77. package/types/components/droparea/styles/index.d.ts +3 -2
  78. package/types/components/droparea/styles/index.d.ts.map +1 -1
  79. package/types/components/emit/components/emit.d.ts +15 -3
  80. package/types/components/emit/components/emit.d.ts.map +1 -1
  81. package/types/components/list/components/item.d.ts +8 -1
  82. package/types/components/list/components/item.d.ts.map +1 -1
  83. package/types/components/list/components/list.d.ts +23 -5
  84. package/types/components/list/components/list.d.ts.map +1 -1
  85. package/types/components/paginator/components/paginator.d.ts +32 -8
  86. package/types/components/paginator/components/paginator.d.ts.map +1 -1
  87. package/types/components/paginator/styles/index.d.ts +3 -2
  88. package/types/components/paginator/styles/index.d.ts.map +1 -1
  89. package/types/components/spinner/components/spinner.d.ts +14 -3
  90. package/types/components/spinner/components/spinner.d.ts.map +1 -1
  91. package/types/components/spinner/styles/index.d.ts +3 -2
  92. package/types/components/spinner/styles/index.d.ts.map +1 -1
  93. package/types/components/splitview/components/splitview.d.ts +22 -4
  94. package/types/components/splitview/components/splitview.d.ts.map +1 -1
  95. package/types/components/splitview/components/splitview.detail.d.ts +12 -2
  96. package/types/components/splitview/components/splitview.detail.d.ts.map +1 -1
  97. package/types/components/splitview/components/splitview.master.d.ts +12 -1
  98. package/types/components/splitview/components/splitview.master.d.ts.map +1 -1
  99. package/types/components/translate/components/translate.d.ts +44 -10
  100. package/types/components/translate/components/translate.d.ts.map +1 -1
@@ -1,3 +1,6 @@
1
1
  import styles from './styles.js'
2
2
 
3
- export default styles
3
+ /** @type {string} */
4
+ const stylesText = styles
5
+
6
+ export default stylesText
@@ -5,7 +5,8 @@ const fallbackRegistry = new Map()
5
5
 
6
6
  /** @param {string} tag
7
7
  * @param {CustomElementConstructor} element
8
- * @param {string} styles **/
8
+ * @param {string} [styles]
9
+ * @returns {CSSStyleSheet|HTMLStyleElement|undefined} */
9
10
  export function define (tag, element, styles = '') {
10
11
  const definedElement = globalThis.customElements.get(tag)
11
12
  if (!definedElement) {
@@ -1,13 +1,17 @@
1
1
  /**
2
- * Convert Strings from camelCase to kebab-case
3
- * @param {string} input @returns {string} */
2
+ * Convert Strings from camelCase to kebab-case.
3
+ * @param {string} input
4
+ * @returns {string}
5
+ */
4
6
  export function camelToKebab (input) {
5
7
  return input.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
6
8
  }
7
9
 
8
10
  /**
9
- * Convert Strings from kebab-case to camelCase
10
- * @param {string} input @returns {string} */
11
+ * Convert Strings from kebab-case to camelCase.
12
+ * @param {string} input
13
+ * @returns {string}
14
+ */
11
15
  export function kebabToCamel (input) {
12
16
  return input.replace(/-([a-z])/g, function (g) {
13
17
  return g[1].toUpperCase()
@@ -15,8 +19,10 @@ export function kebabToCamel (input) {
15
19
  }
16
20
 
17
21
  /**
18
- * Convert Strings from snake to camelCase
19
- * @param {string} input @returns {string} */
22
+ * Convert Strings from snake to camelCase.
23
+ * @param {string} input
24
+ * @returns {string}
25
+ */
20
26
  export function snakeToCamel (input) {
21
27
  return input.replace(/_([a-z])/g, function (g) {
22
28
  return g[1].toUpperCase()
@@ -1,6 +1,13 @@
1
+ /**
2
+ * @import { Component } from '../component/component.js'
3
+ */
1
4
  import { camelToKebab } from './format.js'
2
5
 
3
- /** @param {HTMLElement} self */
6
+ /**
7
+ * Binds declarative event handlers in descendant nodes for a component.
8
+ * @param {Component|HTMLElement} self
9
+ * @returns {void}
10
+ */
4
11
  export function listen (self) {
5
12
  /** @ts-ignore */
6
13
  const binding = self.binding
@@ -62,7 +69,11 @@ export function listen (self) {
62
69
  }
63
70
  }
64
71
 
65
- /** @param {HTMLElement} self */
72
+ /**
73
+ * Resolve dependencies from descendants using `resolve` events.
74
+ * @param {Component|HTMLElement} self
75
+ * @returns {void}
76
+ */
66
77
  function provide (self) {
67
78
  /** @ts-ignore */
68
79
  if (!self.provide) return
@@ -102,7 +113,13 @@ function transform (pipe, value) {
102
113
  }[pipe?.toLowerCase() || 'string'](value)
103
114
  }
104
115
 
105
- /** @param {object} object @param {string} path @param {any} value */
116
+ /**
117
+ * Sets a nested property value by object path.
118
+ * @param {unknown} object
119
+ * @param {string} path
120
+ * @param {any} value
121
+ * @returns {void}
122
+ */
106
123
  export function set (object, path, value) {
107
124
  const pathArray = path.match(/([^[.\]])+/g)
108
125
  if (!pathArray?.length) return
@@ -114,7 +131,13 @@ export function set (object, path, value) {
114
131
  }, object)
115
132
  }
116
133
 
117
- /** @param {object} object @param {string} path @param {any} fallback */
134
+ /**
135
+ * Reads a nested property value by path.
136
+ * @param {unknown} object
137
+ * @param {string} path
138
+ * @param {any} fallback
139
+ * @returns {any}
140
+ */
118
141
  export function get (object, path, fallback) {
119
142
  const pathArray = path.match(/([^[.\]])+/g)
120
143
  if (!pathArray?.length) return fallback
@@ -125,7 +148,10 @@ export function get (object, path, fallback) {
125
148
  return value === undefined ? fallback : value
126
149
  }
127
150
 
128
- /** @param {object} object @return {string} */
151
+ /** @param {object} object
152
+ * @return {string}
153
+ * @description Returns truthy CSS class names from object values.
154
+ */
129
155
  export function keys (object) {
130
156
  return Object.keys(object).filter(
131
157
  key => Boolean(object[key])).join(' ')
@@ -1,3 +1,4 @@
1
+ /** Utility helpers for component internals and runtime bindings. */
1
2
  export * from './define.js'
2
3
  export * from './format.js'
3
4
  export * from './helpers.js'
@@ -1,7 +1,8 @@
1
1
  /**
2
+ * Groups child nodes by slot name.
2
3
  * @param {HTMLElement} container
3
- * @return {Object<string, Array<HTMLElement>>}
4
- * */
4
+ * @returns {Record<string, HTMLElement[]>}
5
+ */
5
6
  export function getSlots (container) {
6
7
  const slots = { general: [] }
7
8
 
@@ -1,4 +1,4 @@
1
- /** @returns {string} */
1
+ /** @returns {string} Generated RFC-4122-like identifier. */
2
2
  export function uuid () {
3
3
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
4
4
  /[xy]/g, function (c) {
@@ -2,8 +2,16 @@ import { Component } from '#base/index.js'
2
2
  import styles from '../styles/index.js'
3
3
 
4
4
  const tag = 'ark-audio'
5
+ /**
6
+ * Audio recorder component.
7
+ * Emits:
8
+ * - `error` with `Error` detail on capture/rendering issues.
9
+ */
5
10
  export class Audio extends Component {
11
+ /** @param {object} context
12
+ * @returns {this} */
6
13
  init (context = {}) {
14
+ /** @type {'idle'|'recording'|'done'} */
7
15
  this.status = 'idle'
8
16
  this.dataURL = null
9
17
  this.timerId = null
@@ -18,6 +26,7 @@ export class Audio extends Component {
18
26
  return ['status']
19
27
  }
20
28
 
29
+ /** @returns {this} */
21
30
  render () {
22
31
  if (this.status === 'done') {
23
32
  this.content = `
@@ -45,7 +54,8 @@ export class Audio extends Component {
45
54
  return super.render()
46
55
  }
47
56
 
48
- /** @param {Event} event */
57
+ /** @param {Event} event
58
+ * @returns {Promise<void>} */
49
59
  async start (event) {
50
60
  event.stopPropagation()
51
61
  this.status = 'recording'
@@ -60,7 +70,8 @@ export class Audio extends Component {
60
70
  this.recorder.start()
61
71
  }
62
72
 
63
- /** @param {Event} event */
73
+ /** @param {Event} event
74
+ * @returns {void} */
64
75
  stop (event) {
65
76
  event.stopPropagation()
66
77
  this.status = 'done'
@@ -70,6 +81,7 @@ export class Audio extends Component {
70
81
  this.render()
71
82
  }
72
83
 
84
+ /** @returns {void} */
73
85
  reset () {
74
86
  clearInterval(this.timerId)
75
87
  this._revokeObjectURL()
@@ -80,6 +92,7 @@ export class Audio extends Component {
80
92
  this.recorder = null
81
93
  }
82
94
 
95
+ /** @returns {void} */
83
96
  disconnectedCallback () {
84
97
  clearInterval(this.timerId)
85
98
  this.recorder?.stream?.getTracks?.().forEach(track => track.stop())
@@ -87,6 +100,7 @@ export class Audio extends Component {
87
100
  super.disconnectedCallback()
88
101
  }
89
102
 
103
+ /** @returns {ReturnType<typeof setInterval>} Interval handle. */
90
104
  _time () {
91
105
  let count = 0
92
106
  return setInterval(() => {
@@ -113,6 +127,7 @@ export class Audio extends Component {
113
127
  reader.onloadend = () => { this.dataURL = reader.result }
114
128
  }
115
129
 
130
+ /** @returns {void} */
116
131
  _revokeObjectURL () {
117
132
  if (!this.objectURL) return
118
133
  this.global.URL.revokeObjectURL?.(this.objectURL)
@@ -1 +1,2 @@
1
+ /** Audio recording component. */
1
2
  export { Audio } from './components/audio.js'
@@ -1,2 +1,6 @@
1
1
  import styles from './ark.css.js'
2
- export default styles
2
+
3
+ /** @type {string} */
4
+ const stylesText = styles
5
+
6
+ export default stylesText
@@ -2,7 +2,12 @@ import { Component } from '#base/index.js'
2
2
  import styles from '../styles/index.js'
3
3
 
4
4
  const tag = 'ark-camera'
5
+ /**
6
+ * Camera capture component.
7
+ */
5
8
  export class Camera extends Component {
9
+ /** @param {object} context
10
+ * @returns {this} */
6
11
  init (context = {}) {
7
12
  this.width = this.width || context.width || 320
8
13
  this.height = this.height || context.height || 320
@@ -47,6 +52,7 @@ export class Camera extends Component {
47
52
  return canvas.toDataURL('image/jpg')
48
53
  }
49
54
 
55
+ /** @returns {Promise<void>} */
50
56
  async start () {
51
57
  const stream = await this.global.navigator.mediaDevices.getUserMedia({
52
58
  video: {
@@ -60,18 +66,22 @@ export class Camera extends Component {
60
66
  this.video.srcObject = stream
61
67
  }
62
68
 
69
+ /** @returns {void} */
63
70
  stop () {
64
71
  // @ts-ignore
65
72
  const tracks = this.video.srcObject ? this.video.srcObject.getTracks() : []
66
73
  tracks.forEach(track => track.stop())
67
74
  }
68
75
 
76
+ /** @param {string} facingMode
77
+ * @returns {Promise<void>} */
69
78
  async setCameraOrientation (facingMode) {
70
79
  this.stop()
71
80
  this.facingMode = facingMode
72
81
  await this.start()
73
82
  }
74
83
 
84
+ /** @returns {void} */
75
85
  disconnectedCallback () {
76
86
  this.stop()
77
87
  super.disconnectedCallback()
@@ -1 +1,2 @@
1
+ /** Camera capture component. */
1
2
  export { Camera } from './components/camera.js'
@@ -1,2 +1,6 @@
1
1
  import styles from './ark.css.js'
2
- export default styles
2
+
3
+ /** @type {string} */
4
+ const stylesText = styles
5
+
6
+ export default stylesText
@@ -1,6 +1,7 @@
1
1
  import { Component } from "#base/index.js"
2
2
 
3
3
  const tag = 'ark-capture'
4
+ /** Template-driven renderer component. */
4
5
  export class Capture extends Component {
5
6
  constructor () {
6
7
  super()
@@ -12,7 +13,8 @@ export class Capture extends Component {
12
13
  return ['receive']
13
14
  }
14
15
 
15
- /** @param {Object} context */
16
+ /** @param {object} context
17
+ * @returns {this} */
16
18
  init (context = {}) {
17
19
  const data = this._parseJSON(this._pop(':scope > data')?.textContent)
18
20
  this.source = /** @type {object} */ (
@@ -23,6 +25,7 @@ export class Capture extends Component {
23
25
  return super.init()
24
26
  }
25
27
 
28
+ /** @returns {this} */
26
29
  render () {
27
30
  const outputTemplate = this._pop(':scope > template')?.innerHTML
28
31
  this.template = (
@@ -36,11 +39,16 @@ export class Capture extends Component {
36
39
  return super.render()
37
40
  }
38
41
 
42
+ /** @param {{detail:any}} event */
39
43
  handle (event) {
40
44
  const source = event.detail
41
45
  this.init({ source }).render()
42
46
  }
43
47
 
48
+ /**
49
+ * @param {string} template
50
+ * @returns {(data: any) => string}
51
+ */
44
52
  _format (template) {
45
53
  let render = null
46
54
 
@@ -62,12 +70,20 @@ export class Capture extends Component {
62
70
  }
63
71
  }
64
72
 
73
+ /**
74
+ * @param {string} selector
75
+ * @returns {HTMLElement|null}
76
+ */
65
77
  _pop (selector) {
66
78
  const element = this.querySelector(selector)
67
79
  element?.remove()
68
- return element
80
+ return /** @type {HTMLElement|null} */ (element)
69
81
  }
70
82
 
83
+ /**
84
+ * @param {string|null} source
85
+ * @returns {object|null}
86
+ */
71
87
  _parseJSON (source) {
72
88
  if (!source) return null
73
89
 
@@ -1 +1,2 @@
1
+ /** Template capture component. */
1
2
  export { Capture } from './components/capture.js'
@@ -4,6 +4,9 @@ import './droparea.js'
4
4
  /** @import {Droparea} from './droparea.js' */
5
5
 
6
6
  const tag = 'ark-droparea-preview'
7
+ /**
8
+ * Renders file previews and drag-sort ordering.
9
+ */
7
10
 
8
11
  export class DropareaPreview extends Component {
9
12
  constructor () {
@@ -12,15 +15,19 @@ export class DropareaPreview extends Component {
12
15
  this._onDragEnd = this.handleDrop.bind(this)
13
16
  }
14
17
 
15
- init (_context = {}) {
18
+ /** @param {object} context
19
+ * @returns {this} */
20
+ init (context = {}) {
16
21
  return super.init()
17
22
  }
18
23
 
24
+ /** @returns {void} */
19
25
  disconnectedCallback () {
20
26
  this.revokeAllFiles()
21
27
  super.disconnectedCallback()
22
28
  }
23
29
 
30
+ /** @returns {this} */
24
31
  render () {
25
32
  this.content = /* html */ `
26
33
  <ul data-preview-list class="ark-droparea-preview__list drag-sort-enable"></ul>
@@ -28,6 +35,10 @@ export class DropareaPreview extends Component {
28
35
  return super.render()
29
36
  }
30
37
 
38
+ /**
39
+ * @param {File} file
40
+ * @returns {void}
41
+ */
31
42
  previewFile (file) {
32
43
  const blobUrl = this.getObjectURL(file)
33
44
  const fileType = file.type.split('/')[0]
@@ -58,6 +69,7 @@ export class DropareaPreview extends Component {
58
69
  }
59
70
  }
60
71
 
72
+ /** @returns {void} */
61
73
  toggleVisibility () {
62
74
  const previewZone = this.select('[data-preview-list]')
63
75
  this.files.length !== 0
@@ -67,6 +79,8 @@ export class DropareaPreview extends Component {
67
79
 
68
80
  /* DragSort Functionality */
69
81
 
82
+ /** @param {string} listClass
83
+ * @returns {void} */
70
84
  enableDragSort (listClass) {
71
85
  const sortableLists = this.getElementsByClassName(listClass)
72
86
  Array.prototype.map.call(sortableLists, (list) => {
@@ -74,56 +88,69 @@ export class DropareaPreview extends Component {
74
88
  })
75
89
  }
76
90
 
91
+ /** @param {HTMLUListElement} list
92
+ * @returns {void} */
77
93
  enableDragList (list) {
78
94
  Array.prototype.map.call(list.children, (item) => {
79
95
  this.enableDragItem(item)
80
96
  })
81
97
  }
82
98
 
99
+ /** @param {HTMLElement} item
100
+ * @returns {void} */
83
101
  enableDragItem (item) {
84
102
  if (item.hasAttribute('data-drag-enabled')) return
85
103
 
86
104
  item.setAttribute('data-drag-enabled', '')
87
- item.setAttribute('draggable', true)
105
+ item.setAttribute('draggable', 'true')
88
106
  item.addEventListener('drag', this.handleDrag.bind(this, item))
89
107
  item.addEventListener('dragend', this._onDragEnd, false)
90
108
  }
91
109
 
92
110
  /* istanbul ignore next */
111
+ /** @param {HTMLLIElement} item
112
+ * @param {DragEvent} event
113
+ * @returns {void} */
93
114
  handleDrag (item, event) {
94
115
  const selectedItem = item
95
- const list = selectedItem.parentNode
116
+ const list = /** @type {HTMLElement} */ (selectedItem.parentElement)
96
117
  const x = event.clientX
97
118
  const y = event.clientY
98
119
 
99
120
  selectedItem.classList.add('drag-sort-active')
100
- let swapItem =
101
- document.elementFromPoint(x, y) === null
102
- ? selectedItem
103
- : document.elementFromPoint(x, y)
104
- if (list === swapItem.parentNode) {
121
+ let swapItem = /** @type {ChildNode|null} */ (
122
+ document.elementFromPoint(x, y))
123
+ if (!swapItem) swapItem = selectedItem
124
+
125
+ if (list && swapItem && list === swapItem.parentNode) {
105
126
  swapItem = (
106
127
  swapItem !== selectedItem.nextSibling
107
128
  ? swapItem
108
129
  : swapItem.nextSibling)
109
- list.insertBefore(selectedItem, swapItem)
130
+ list.insertBefore(selectedItem, /** @type {ChildNode|null} */ (swapItem))
110
131
  }
111
132
  }
112
133
 
134
+ /** @param {DragEvent} event
135
+ * @returns {void} */
113
136
  handleDrop (event) {
114
- const droparea = event.target.closest('ark-droparea')
137
+ const target = /** @type {HTMLElement|null} */ (event.target)
138
+ if (!target) return
139
+ const droparea = /** @type {Droparea|null} */ (target.closest('ark-droparea'))
115
140
  if (!droparea) return
116
141
 
117
142
  droparea.preview.createNewFileList()
118
- event.target.classList.remove('drag-sort-active')
143
+ target.classList.remove('drag-sort-active')
119
144
  droparea.preview.dispatchAlterEvent()
120
145
  }
121
146
  /* ---------------------------------------------------- */
122
147
 
148
+ /** @returns {void} */
123
149
  dispatchAlterEvent () {
124
150
  this.emit('alter', this.mediaList)
125
151
  }
126
152
 
153
+ /** @returns {void} */
127
154
  createNewFileList () {
128
155
  const nodeList = this.querySelectorAll('li')
129
156
  const newList = []
@@ -134,10 +161,16 @@ export class DropareaPreview extends Component {
134
161
  this.droparea.fileList = newList
135
162
  }
136
163
 
164
+ /**
165
+ * @param {File} file
166
+ * @returns {boolean}
167
+ */
137
168
  fileExists (file) {
138
169
  return this.files.some((item) => item.name === file.name)
139
170
  }
140
171
 
172
+ /** @param {File} file
173
+ * @returns {string} */
141
174
  getObjectURL (file) {
142
175
  if (this._objectUrls.has(file)) return this._objectUrls.get(file)
143
176
 
@@ -146,6 +179,8 @@ export class DropareaPreview extends Component {
146
179
  return url
147
180
  }
148
181
 
182
+ /** @param {File} file
183
+ * @returns {void} */
149
184
  revokeFile (file) {
150
185
  const url = this._objectUrls.get(file)
151
186
  if (!url) return
@@ -154,6 +189,7 @@ export class DropareaPreview extends Component {
154
189
  URL.revokeObjectURL?.(url)
155
190
  }
156
191
 
192
+ /** @returns {void} */
157
193
  revokeAllFiles () {
158
194
  for (const url of this._objectUrls.values()) {
159
195
  URL.revokeObjectURL?.(url)
@@ -161,20 +197,25 @@ export class DropareaPreview extends Component {
161
197
  this._objectUrls.clear()
162
198
  }
163
199
 
200
+ /** @returns {void} */
164
201
  clearPreview () {
165
202
  const previewZone = this.select('[data-preview-list]')
166
203
  previewZone && (previewZone.textContent = '')
167
204
  this.toggleVisibility()
168
205
  }
169
206
 
207
+ /** @param {File} file
208
+ * @param {MouseEvent} event
209
+ * @returns {void} */
170
210
  removeFile (file, event) {
171
- const element = event.target
211
+ const element = /** @type {HTMLElement|null} */ (event.target)
212
+ if (!element) return
172
213
  const fileIndex = this.droparea.fileList.indexOf(file)
173
214
  if (fileIndex < 0) return
174
215
 
175
216
  this.revokeFile(file)
176
217
  this.droparea.fileList.splice(fileIndex, 1)
177
- element.parentNode.remove()
218
+ element.parentElement?.remove()
178
219
  this.selectAll('li').forEach((item, index) =>
179
220
  item.setAttribute('index', String(index))
180
221
  )
@@ -182,14 +223,18 @@ export class DropareaPreview extends Component {
182
223
  this.dispatchAlterEvent()
183
224
  }
184
225
 
226
+ /** @param {File} file
227
+ * @returns {number} */
185
228
  fileIndex (file) {
186
229
  return this.droparea.fileList.indexOf(file)
187
230
  }
188
231
 
232
+ /** @returns {Droparea} */
189
233
  get droparea () {
190
234
  return /** @type {Droparea} */ (this.closest('ark-droparea'))
191
235
  }
192
236
 
237
+ /** @returns {Array<{name:string,type:string,size:number,url:string}>} */
193
238
  get mediaList () {
194
239
  const mediaList = []
195
240
  this.droparea.fileList.map((file) => {
@@ -287,3 +287,85 @@ it('does nothing when revoking a file without an object URL', () => {
287
287
  preview.revokeFile(file)
288
288
  })
289
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
+ }
371
+ })