treeselectjs 0.2.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.
@@ -0,0 +1,318 @@
1
+ import TreeselectInput from "./input.js"
2
+ import TreeselectList from "./list.js"
3
+
4
+ class Treeselect {
5
+ // Components
6
+ #htmlContainer = null
7
+ #treeselectList = null
8
+ #treeselectInput = null
9
+
10
+ // Resize props
11
+ #transform = { top: null, bottom: null }
12
+ #treeselectInitPosition = null
13
+ #containerResizer = null
14
+ #containerWidth = 0
15
+
16
+ constructor ({
17
+ parentHtmlContainer,
18
+ value,
19
+ options,
20
+ openLevel,
21
+ appendToBody,
22
+ alwaysOpen,
23
+ showTags,
24
+ clearable,
25
+ searchable,
26
+ placeholder,
27
+ grouped,
28
+ listSlotHtmlComponent,
29
+ disabled,
30
+ emptyText
31
+ }) {
32
+ this.parentHtmlContainer = parentHtmlContainer
33
+ this.value = value ?? []
34
+ this.options = options ?? []
35
+ this.openLevel = openLevel ?? 0
36
+ this.appendToBody = appendToBody ?? true
37
+ this.alwaysOpen = alwaysOpen && !disabled
38
+ this.showTags = showTags ?? true
39
+ this.clearable = clearable ?? true
40
+ this.searchable = searchable ?? true
41
+ this.placeholder = placeholder ?? 'Search...'
42
+ this.grouped = grouped ?? true
43
+ this.listSlotHtmlComponent = listSlotHtmlComponent ?? null
44
+ this.disabled = disabled ?? false
45
+ this.emptyText = emptyText
46
+
47
+ this.srcElement = null
48
+
49
+ // Outside listeners
50
+ this.scrollEvent = null
51
+ this.focusEvent = null
52
+ this.blurEvent = null
53
+
54
+ this.mount()
55
+ }
56
+
57
+ // Public methods
58
+ mount () {
59
+ if (this.srcElement) {
60
+ this.#closeList()
61
+ this.srcElement.innerHTML = ''
62
+ this.srcElement = null
63
+ this.#removeOutsideListeners()
64
+ }
65
+
66
+ this.srcElement = this.#createTreeselect()
67
+
68
+ this.scrollEvent = this.scrollWindowHandler.bind(this)
69
+ this.focusEvent = this.focusWindowHandler.bind(this)
70
+ this.blurEvent = this.blurWindowHandler.bind(this)
71
+
72
+ if (this.alwaysOpen) {
73
+ this.#treeselectInput.openClose()
74
+ }
75
+
76
+ if (this.disabled) {
77
+ this.srcElement.classList.add('treeselect--disabled')
78
+ }
79
+ }
80
+
81
+ updateValue (newValue) {
82
+ const list = this.#treeselectList
83
+ list.updateValue(newValue)
84
+ const {groupedIds, ids } = list.selectedNodes
85
+ const inputNewValue = this.grouped ? groupedIds : ids
86
+ this.#treeselectInput.updateValue(inputNewValue)
87
+ }
88
+
89
+ #createTreeselect () {
90
+ const container = this.parentHtmlContainer
91
+ container.classList.add('treeselect')
92
+
93
+ const list = new TreeselectList({
94
+ options: this.options,
95
+ value: this.value,
96
+ openLevel: this.openLevel,
97
+ listSlotHtmlComponent: this.listSlotHtmlComponent,
98
+ emptyText: this.emptyText
99
+ })
100
+
101
+ const {groupedIds, ids } = list.selectedNodes
102
+ const input = new TreeselectInput({
103
+ value: this.grouped ? groupedIds : ids,
104
+ showTags: this.showTags,
105
+ clearable: this.clearable,
106
+ isAlwaysOpened: this.alwaysOpen,
107
+ searchable: this.searchable,
108
+ placeholder: this.placeholder,
109
+ disabled: this.disabled
110
+ })
111
+
112
+ if (this.appendToBody) {
113
+ this.#containerResizer = new ResizeObserver(() => {
114
+ const { width } = this.srcElement.getBoundingClientRect()
115
+ this.#containerWidth = width
116
+ this.updateListPosition(container, list.srcElement, true)
117
+ })
118
+ }
119
+
120
+ // Input events
121
+ input.srcElement.addEventListener('input', (e) => {
122
+ const ids = e.detail.map(({ id }) => id)
123
+ this.value = ids
124
+ list.updateValue(ids)
125
+ this.#emitInput()
126
+ })
127
+ input.srcElement.addEventListener('open', () => this.#openList())
128
+ input.srcElement.addEventListener('keydown', (e) => list.callKeyAction(e.key))
129
+ input.srcElement.addEventListener('search', (e) => {
130
+ list.updateSearchValue(e.detail)
131
+ this.updateListPosition(container, list.srcElement, true)
132
+ })
133
+ input.srcElement.addEventListener('focus', () => {
134
+ this.#updateFocusClasses(true)
135
+ document.addEventListener('mousedown', this.focusEvent, true)
136
+ document.addEventListener('focus', this.focusEvent, true)
137
+ window.addEventListener('blur', this.blurEvent)
138
+ }, true)
139
+
140
+ if (!this.alwaysOpen) {
141
+ input.srcElement.addEventListener('close', () => {
142
+ this.#closeList()
143
+ })
144
+ }
145
+
146
+ // List events
147
+ list.srcElement.addEventListener('input', (e) => {
148
+ const {groupedIds, ids } = e.detail
149
+ const inputIds = this.grouped ? groupedIds : ids
150
+ input.updateValue(inputIds)
151
+ this.value = ids.map(({ id }) => id)
152
+ input.focus()
153
+ this.#emitInput()
154
+ })
155
+ list.srcElement.addEventListener('arrow-click', () => {
156
+ input.focus()
157
+ this.updateListPosition(container, list.srcElement, true)
158
+ })
159
+
160
+ this.#htmlContainer = container
161
+ this.#treeselectList = list
162
+ this.#treeselectInput = input
163
+
164
+ container.append(input.srcElement)
165
+
166
+ return container
167
+ }
168
+
169
+ #openList () {
170
+ window.addEventListener('scroll', this.scrollEvent, true)
171
+
172
+ if (this.appendToBody) {
173
+ document.body.appendChild(this.#treeselectList.srcElement)
174
+ this.#containerResizer.observe(this.#htmlContainer)
175
+ } else {
176
+ this.#htmlContainer.appendChild(this.#treeselectList.srcElement)
177
+ }
178
+
179
+ this.updateListPosition(this.#htmlContainer, this.#treeselectList.srcElement, false)
180
+ this.#updateOpenCloseClasses(true)
181
+ this.#treeselectList.focusFirstListElement()
182
+ }
183
+
184
+ #closeList () {
185
+ window.removeEventListener('scroll', this.scrollEvent, true)
186
+
187
+ if (this.appendToBody) {
188
+ document.body.removeChild(this.#treeselectList.srcElement)
189
+ this.#containerResizer?.disconnect()
190
+ } else {
191
+ this.#htmlContainer.removeChild(this.#treeselectList.srcElement)
192
+ }
193
+
194
+ this.#updateOpenCloseClasses(false)
195
+ }
196
+
197
+ #updateDirectionClasses (isTop, appendToBody) {
198
+ const topClass = appendToBody ? 'treeselect-list--top-to-body' : 'treeselect-list--top'
199
+ const bottomClass = appendToBody ? 'treeselect-list--bottom-to-body' : 'treeselect-list--bottom'
200
+
201
+ if (isTop) {
202
+ this.#treeselectList.srcElement.classList.add(topClass)
203
+ this.#treeselectList.srcElement.classList.remove(bottomClass)
204
+ this.#treeselectInput.srcElement.classList.add('treeselect-input--top')
205
+ this.#treeselectInput.srcElement.classList.remove('treeselect-input--bottom')
206
+ } else {
207
+ this.#treeselectList.srcElement.classList.remove(topClass)
208
+ this.#treeselectList.srcElement.classList.add(bottomClass)
209
+ this.#treeselectInput.srcElement.classList.remove('treeselect-input--top')
210
+ this.#treeselectInput.srcElement.classList.add('treeselect-input--bottom')
211
+ }
212
+ }
213
+
214
+ #updateFocusClasses (isFocus) {
215
+ if (isFocus) {
216
+ this.#treeselectInput.srcElement.classList.add('treeselect-input--focused')
217
+ this.#treeselectList.srcElement.classList.add('treeselect-list--focused')
218
+ } else {
219
+ this.#treeselectInput.srcElement.classList.remove('treeselect-input--focused')
220
+ this.#treeselectList.srcElement.classList.remove('treeselect-list--focused')
221
+ }
222
+ }
223
+
224
+ #updateOpenCloseClasses (isOpen) {
225
+ if (isOpen) {
226
+ this.#treeselectInput.srcElement.classList.add('treeselect-input--opened')
227
+ } else {
228
+ this.#treeselectInput.srcElement.classList.remove('treeselect-input--opened')
229
+ }
230
+ }
231
+
232
+ #removeOutsideListeners () {
233
+ window.removeEventListener('scroll', this.scrollEvent, true)
234
+
235
+ document.removeEventListener('click', this.focusEvent, true)
236
+ document.removeEventListener('focus', this.focusEvent, true)
237
+ window.removeEventListener('blur', this.blurEvent)
238
+ }
239
+
240
+ // Outside Listeners
241
+ scrollWindowHandler () {
242
+ this.updateListPosition(this.#htmlContainer, this.#treeselectList.srcElement, false)
243
+ }
244
+
245
+ focusWindowHandler (e) {
246
+ const isInsideClick = this.#htmlContainer.contains(e.target) || this.#treeselectList.srcElement.contains(e.target)
247
+
248
+ if (!isInsideClick) {
249
+ this.#treeselectInput.blur()
250
+ this.#removeOutsideListeners()
251
+ this.#updateFocusClasses(false)
252
+ }
253
+ }
254
+
255
+ blurWindowHandler () {
256
+ this.#treeselectInput.blur()
257
+ this.#removeOutsideListeners()
258
+ this.#updateFocusClasses(false)
259
+ }
260
+
261
+ // Update direction of the list. Support appendToBody and standart mode with absolute
262
+ updateListPosition (container, list, isNeedForceUpdate) {
263
+ const spaceTop = container.getBoundingClientRect().y
264
+ const spaceBottom = window.innerHeight - container.getBoundingClientRect().y
265
+ const listHeight = list.clientHeight
266
+ const spaceDelta = 45
267
+ const isTopDirection = spaceTop > spaceBottom && window.innerHeight - spaceTop < listHeight + spaceDelta
268
+ const attributeToAdd = isTopDirection ? 'top' : 'buttom'
269
+ const currentAttr = list.getAttribute('direction')
270
+
271
+ this.#htmlContainer.setAttribute('direction', attributeToAdd)
272
+
273
+ // Standart class handler handler with absolute position
274
+ if (!this.appendToBody) {
275
+ const isNoNeedToUpdate = currentAttr === attributeToAdd
276
+
277
+ if (isNoNeedToUpdate) {
278
+ return
279
+ }
280
+
281
+ this.#updateDirectionClasses(isTopDirection, false)
282
+
283
+ return
284
+ }
285
+
286
+ // Append to body handler
287
+ if (!this.#treeselectInitPosition || isNeedForceUpdate) {
288
+ list.style.transform = null
289
+
290
+ const { x: listX, y: listY } = list.getBoundingClientRect()
291
+ const { x: containerX, y: containerY } = container.getBoundingClientRect()
292
+
293
+ this.#treeselectInitPosition = { containerX, containerY, listX, listY }
294
+ }
295
+
296
+ const { listX, listY, containerX, containerY } = this.#treeselectInitPosition
297
+ const containerHeight = container.clientHeight
298
+
299
+ // TODO you should use css max-height
300
+ // list.style.maxHeight = `${window.innerHeight - containerHeight}px`
301
+
302
+ if (!currentAttr || isNeedForceUpdate) {
303
+ this.#transform.top = `translate(${containerX - listX}px, ${containerY - listY - listHeight}px)`
304
+ this.#transform.bottom = `translate(${containerX - listX}px, ${containerY + containerHeight - listY}px)`
305
+ }
306
+
307
+ list.style.transform = isTopDirection ? this.#transform.top : this.#transform.bottom
308
+ this.#updateDirectionClasses(isTopDirection, true)
309
+ list.style.width = `${this.#containerWidth}px`
310
+ }
311
+
312
+ // Emits
313
+ #emitInput () {
314
+ this.srcElement.dispatchEvent(new CustomEvent('input', { detail: this.value }))
315
+ }
316
+ }
317
+
318
+ export default Treeselect