fwtoolkit 0.1.0-alpha.1 → 0.1.0-alpha.3
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.
- package/package.json +10 -2
- package/src/basic.js +500 -0
- package/src/blob.js +1 -1
- package/src/content_menu.js +475 -0
- package/src/datatable_bulk.js +208 -0
- package/src/dialog.js +484 -0
- package/src/events.js +9 -0
- package/src/faq_dialog.js +67 -0
- package/src/file/dialog.js +142 -0
- package/src/file/index.js +9 -0
- package/src/file/new_folder_dialog.js +37 -0
- package/src/file/selector.js +263 -0
- package/src/file/templates.js +11 -0
- package/src/file/tools.js +58 -0
- package/src/focus.js +20 -0
- package/src/index.js +58 -9
- package/src/network.js +63 -14
- package/src/overview_menu.js +611 -0
- package/src/settings.js +18 -0
- package/src/templates.js +42 -0
- package/src/user.js +46 -0
- package/src/user_util.js +16 -0
- package/src/worker.js +12 -0
- package/src/ws.js +347 -0
- package/src/file.js +0 -25
- package/src/text.js +0 -44
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
const menuTemplate = ({
|
|
2
|
+
id,
|
|
3
|
+
classes,
|
|
4
|
+
height,
|
|
5
|
+
width,
|
|
6
|
+
zIndex,
|
|
7
|
+
menu,
|
|
8
|
+
scroll,
|
|
9
|
+
page
|
|
10
|
+
}) =>
|
|
11
|
+
`<div tabindex="-1" role="incontent_menu"
|
|
12
|
+
class="ui-content-menu ui-corner-all ui-widget ui-widget-content ui-front"
|
|
13
|
+
${id ? `aria-describedby="${id}"` : ""} style="z-index: ${zIndex};">
|
|
14
|
+
<div ${id ? `id="${id}"` : ""} class="ui-content-menu-content ui-widget-content${classes ? ` ${classes}` : ""}${scroll ? " ui-scrollable" : ""}" style="width: ${width}; height: ${height};">
|
|
15
|
+
<div>
|
|
16
|
+
<ul class="content-menu-list">
|
|
17
|
+
${menu.content
|
|
18
|
+
.map((menuItem, index) => {
|
|
19
|
+
switch (menuItem.type) {
|
|
20
|
+
case "header":
|
|
21
|
+
return `<li><span class="content-menu-item-header" title="${menuItem.tooltip}">${
|
|
22
|
+
typeof menuItem.title === "function"
|
|
23
|
+
? menuItem.title(page)
|
|
24
|
+
: menuItem.title
|
|
25
|
+
}</span></li>`
|
|
26
|
+
case "separator":
|
|
27
|
+
return '<li><hr class="content-menu-item-divider"/></li>'
|
|
28
|
+
default:
|
|
29
|
+
return `<li tabindex="0" data-index="${index}" class="content-menu-item${
|
|
30
|
+
menuItem.disabled && menuItem.disabled(page)
|
|
31
|
+
? " disabled"
|
|
32
|
+
: menuItem.selected
|
|
33
|
+
? " selected"
|
|
34
|
+
: ""
|
|
35
|
+
}" title='${menuItem.tooltip}'>
|
|
36
|
+
${
|
|
37
|
+
typeof menuItem.title === "function"
|
|
38
|
+
? menuItem.title(page)
|
|
39
|
+
: menuItem.title
|
|
40
|
+
} ${
|
|
41
|
+
menuItem.icon
|
|
42
|
+
? `<span class="content-menu-item-icon"><i class="fa fa-${menuItem.icon}"></i></span>`
|
|
43
|
+
: ""
|
|
44
|
+
}
|
|
45
|
+
</li>`
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.join("")}
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="ui-widget-overlay ui-front" style="z-index: ${zIndex - 1}"></div>`
|
|
54
|
+
|
|
55
|
+
export class ContentMenu {
|
|
56
|
+
constructor({
|
|
57
|
+
id = false,
|
|
58
|
+
page = false,
|
|
59
|
+
classes = false,
|
|
60
|
+
menu = {content: []},
|
|
61
|
+
height = false,
|
|
62
|
+
width = false,
|
|
63
|
+
onClose = false,
|
|
64
|
+
scroll = false,
|
|
65
|
+
dialogEl = false,
|
|
66
|
+
backdropEl = false,
|
|
67
|
+
menuPos = false
|
|
68
|
+
}) {
|
|
69
|
+
this.id = id
|
|
70
|
+
this.page = page
|
|
71
|
+
this.classes = classes
|
|
72
|
+
this.menu = menu
|
|
73
|
+
this.height = height ? `${height}px` : "auto"
|
|
74
|
+
this.width = width ? `${width}px` : "auto"
|
|
75
|
+
this.onClose = onClose
|
|
76
|
+
this.scroll = scroll
|
|
77
|
+
this.dialogEl = dialogEl
|
|
78
|
+
this.backdropEl = backdropEl
|
|
79
|
+
this.menuPos = menuPos
|
|
80
|
+
|
|
81
|
+
this.focusedIndex = 0
|
|
82
|
+
this.previouslyFocusedElement = null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
open() {
|
|
86
|
+
if (this.dialogEl) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.previouslyFocusedElement = document.activeElement
|
|
91
|
+
|
|
92
|
+
document.body.insertAdjacentHTML(
|
|
93
|
+
"beforeend",
|
|
94
|
+
menuTemplate({
|
|
95
|
+
id: this.id,
|
|
96
|
+
classes: this.classes,
|
|
97
|
+
height: this.height,
|
|
98
|
+
width: this.width,
|
|
99
|
+
zIndex: this.getHighestDialogZIndex() + 2,
|
|
100
|
+
menu: this.menu,
|
|
101
|
+
scroll: this.scroll,
|
|
102
|
+
page: this.page
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
this.backdropEl = document.body.lastElementChild
|
|
106
|
+
this.dialogEl = this.backdropEl.previousElementSibling
|
|
107
|
+
if (this.menuPos?.X && this.menuPos?.Y) {
|
|
108
|
+
this.positionDialog()
|
|
109
|
+
} else {
|
|
110
|
+
this.centerDialog()
|
|
111
|
+
}
|
|
112
|
+
this.checkAndAddColumns()
|
|
113
|
+
this.bind()
|
|
114
|
+
this.focusFirstMenuItem()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
renderColumnsHtml(columns) {
|
|
118
|
+
const itemsPerColumn = Math.ceil(this.menu.content.length / columns)
|
|
119
|
+
let html = '<div class="content-menu-columns">'
|
|
120
|
+
for (let col = 0; col < columns; col++) {
|
|
121
|
+
const start = col * itemsPerColumn
|
|
122
|
+
const end = Math.min(
|
|
123
|
+
start + itemsPerColumn,
|
|
124
|
+
this.menu.content.length
|
|
125
|
+
)
|
|
126
|
+
if (start >= this.menu.content.length) {
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
html += '<ul class="content-menu-list">'
|
|
130
|
+
for (let i = start; i < end; i++) {
|
|
131
|
+
const menuItem = this.menu.content[i]
|
|
132
|
+
switch (menuItem.type) {
|
|
133
|
+
case "header":
|
|
134
|
+
html += `<li><span class="content-menu-item-header" title="${menuItem.tooltip}">${
|
|
135
|
+
typeof menuItem.title === "function"
|
|
136
|
+
? menuItem.title(this.page)
|
|
137
|
+
: menuItem.title
|
|
138
|
+
}</span></li>`
|
|
139
|
+
break
|
|
140
|
+
case "separator":
|
|
141
|
+
html +=
|
|
142
|
+
'<li><hr class="content-menu-item-divider"/></li>'
|
|
143
|
+
break
|
|
144
|
+
default:
|
|
145
|
+
html += `<li tabindex="0" data-index="${i}" class="content-menu-item${
|
|
146
|
+
menuItem.disabled && menuItem.disabled(this.page)
|
|
147
|
+
? " disabled"
|
|
148
|
+
: menuItem.selected
|
|
149
|
+
? " selected"
|
|
150
|
+
: ""
|
|
151
|
+
}" title='${menuItem.tooltip}'>
|
|
152
|
+
${
|
|
153
|
+
typeof menuItem.title === "function"
|
|
154
|
+
? menuItem.title(this.page)
|
|
155
|
+
: menuItem.title
|
|
156
|
+
} ${
|
|
157
|
+
menuItem.icon
|
|
158
|
+
? `<span class="content-menu-item-icon"><i class="fa fa-${menuItem.icon}"></i></span>`
|
|
159
|
+
: ""
|
|
160
|
+
}
|
|
161
|
+
</li>`
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
html += "</ul>"
|
|
165
|
+
}
|
|
166
|
+
html += "</div>"
|
|
167
|
+
return html
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
checkAndAddColumns() {
|
|
171
|
+
const dialogRect = this.dialogEl.getBoundingClientRect()
|
|
172
|
+
const viewportHeight = window.innerHeight
|
|
173
|
+
const viewportWidth = window.innerWidth
|
|
174
|
+
const maxHeight = viewportHeight * 0.9
|
|
175
|
+
const maxWidth = viewportWidth * 0.95
|
|
176
|
+
|
|
177
|
+
if (dialogRect.height >= maxHeight) {
|
|
178
|
+
const contentEl = this.dialogEl.querySelector(
|
|
179
|
+
".ui-content-menu-content"
|
|
180
|
+
)
|
|
181
|
+
const contentDiv = contentEl.querySelector(":scope > div")
|
|
182
|
+
let columns = 2
|
|
183
|
+
while (columns <= 6) {
|
|
184
|
+
contentDiv.innerHTML = this.renderColumnsHtml(columns)
|
|
185
|
+
const columnsDiv = contentDiv.querySelector(
|
|
186
|
+
".content-menu-columns"
|
|
187
|
+
)
|
|
188
|
+
if (columnsDiv) {
|
|
189
|
+
const naturalWidth = columnsDiv.scrollWidth + 20
|
|
190
|
+
contentEl.style.width = `${naturalWidth}px`
|
|
191
|
+
}
|
|
192
|
+
const newRect = this.dialogEl.getBoundingClientRect()
|
|
193
|
+
if (newRect.height < maxHeight && newRect.width < maxWidth) {
|
|
194
|
+
if (this.menuPos?.X && this.menuPos?.Y) {
|
|
195
|
+
this.positionDialog()
|
|
196
|
+
} else {
|
|
197
|
+
this.centerDialog()
|
|
198
|
+
}
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
columns++
|
|
202
|
+
}
|
|
203
|
+
// Fallback: restore single column with scrolling
|
|
204
|
+
contentDiv.innerHTML = `<ul class="content-menu-list">${this.renderSingleColumnHtml()}</ul>`
|
|
205
|
+
contentEl.style.width = this.width
|
|
206
|
+
this.dialogEl
|
|
207
|
+
.querySelector(".ui-content-menu-content")
|
|
208
|
+
.classList.add("ui-scrollable")
|
|
209
|
+
if (this.menuPos?.X && this.menuPos?.Y) {
|
|
210
|
+
this.positionDialog()
|
|
211
|
+
} else {
|
|
212
|
+
this.centerDialog()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
renderSingleColumnHtml() {
|
|
218
|
+
return this.menu.content
|
|
219
|
+
.map((menuItem, index) => {
|
|
220
|
+
switch (menuItem.type) {
|
|
221
|
+
case "header":
|
|
222
|
+
return `<li><span class="content-menu-item-header" title="${menuItem.tooltip}">${
|
|
223
|
+
typeof menuItem.title === "function"
|
|
224
|
+
? menuItem.title(this.page)
|
|
225
|
+
: menuItem.title
|
|
226
|
+
}</span></li>`
|
|
227
|
+
case "separator":
|
|
228
|
+
return '<li><hr class="content-menu-item-divider"/></li>'
|
|
229
|
+
default:
|
|
230
|
+
return `<li tabindex="0" data-index="${index}" class="content-menu-item${
|
|
231
|
+
menuItem.disabled && menuItem.disabled(this.page)
|
|
232
|
+
? " disabled"
|
|
233
|
+
: menuItem.selected
|
|
234
|
+
? " selected"
|
|
235
|
+
: ""
|
|
236
|
+
}" title='${menuItem.tooltip}'>
|
|
237
|
+
${
|
|
238
|
+
typeof menuItem.title === "function"
|
|
239
|
+
? menuItem.title(this.page)
|
|
240
|
+
: menuItem.title
|
|
241
|
+
} ${
|
|
242
|
+
menuItem.icon
|
|
243
|
+
? `<span class="content-menu-item-icon"><i class="fa fa-${menuItem.icon}"></i></span>`
|
|
244
|
+
: ""
|
|
245
|
+
}
|
|
246
|
+
</li>`
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
.join("")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
centerDialog() {
|
|
253
|
+
const totalWidth = window.innerWidth,
|
|
254
|
+
totalHeight = window.innerHeight,
|
|
255
|
+
dialogRect = this.dialogEl.getBoundingClientRect(),
|
|
256
|
+
dialogWidth = dialogRect.width + 10,
|
|
257
|
+
dialogHeight = dialogRect.height + 10,
|
|
258
|
+
scrollTopOffset = window.pageYOffset,
|
|
259
|
+
scrollLeftOffset = window.pageXOffset
|
|
260
|
+
this.dialogEl.style.top = `${(totalHeight - dialogHeight) / 2 + scrollTopOffset}px`
|
|
261
|
+
this.dialogEl.style.left = `${(totalWidth - dialogWidth) / 2 + scrollLeftOffset}px`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
positionDialog() {
|
|
265
|
+
const dialogHeight = this.dialogEl.getBoundingClientRect().height + 10,
|
|
266
|
+
dialogWidth = this.dialogEl.getBoundingClientRect().width + 10,
|
|
267
|
+
scrollTopOffset = window.pageYOffset,
|
|
268
|
+
clientHeight = window.document.documentElement.clientHeight,
|
|
269
|
+
clientWidth = window.document.documentElement.clientWidth
|
|
270
|
+
|
|
271
|
+
// We try to ensure that the menu is seen in the browser at the preferred location.
|
|
272
|
+
// Adjustments are made in case it doesn't fit.
|
|
273
|
+
let top = this.menuPos.Y,
|
|
274
|
+
left = this.menuPos.X
|
|
275
|
+
|
|
276
|
+
if (top + dialogHeight > scrollTopOffset + clientHeight) {
|
|
277
|
+
top -= top + dialogHeight - (scrollTopOffset + clientHeight)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (top < scrollTopOffset) {
|
|
281
|
+
top = scrollTopOffset + 10
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (left + dialogWidth > clientWidth) {
|
|
285
|
+
left -= left + dialogWidth - clientWidth
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.dialogEl.style.top = `${top}px`
|
|
289
|
+
this.dialogEl.style.left = `${left}px`
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
bind() {
|
|
293
|
+
this.backdropEl.addEventListener("click", () => this.close())
|
|
294
|
+
this.dialogEl.addEventListener("click", event => this.onclick(event))
|
|
295
|
+
this.dialogEl.addEventListener("keydown", event =>
|
|
296
|
+
this.onKeyDown(event)
|
|
297
|
+
)
|
|
298
|
+
this.dialogEl.focus()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getHighestDialogZIndex() {
|
|
302
|
+
let zIndex = 100
|
|
303
|
+
document
|
|
304
|
+
.querySelectorAll("div.ui-content-menu")
|
|
305
|
+
.forEach(
|
|
306
|
+
dialogEl => (zIndex = Math.max(zIndex, dialogEl.style.zIndex))
|
|
307
|
+
)
|
|
308
|
+
document
|
|
309
|
+
.querySelectorAll("div.ui-dialog")
|
|
310
|
+
.forEach(
|
|
311
|
+
dialogEl => (zIndex = Math.max(zIndex, dialogEl.style.zIndex))
|
|
312
|
+
)
|
|
313
|
+
return zIndex
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
close() {
|
|
317
|
+
if (!this.dialogEl || !this.dialogEl.parentElement) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
this.dialogEl.parentElement.removeChild(this.dialogEl)
|
|
321
|
+
this.backdropEl.parentElement.removeChild(this.backdropEl)
|
|
322
|
+
|
|
323
|
+
// Restore focus to the previously focused element
|
|
324
|
+
if (
|
|
325
|
+
this.previouslyFocusedElement &&
|
|
326
|
+
this.previouslyFocusedElement.focus
|
|
327
|
+
) {
|
|
328
|
+
this.previouslyFocusedElement.focus()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (this.onClose) {
|
|
332
|
+
this.onClose()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
onclick(event) {
|
|
337
|
+
event.preventDefault()
|
|
338
|
+
event.stopImmediatePropagation()
|
|
339
|
+
const target = event.target.closest("li.content-menu-item")
|
|
340
|
+
if (target) {
|
|
341
|
+
const menuNumber = target.dataset.index
|
|
342
|
+
const menuItem = this.menu.content[menuNumber]
|
|
343
|
+
if (menuItem.disabled?.(this.page)) {
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
menuItem.action(this.page)
|
|
347
|
+
this.close()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
onKeyDown(event) {
|
|
352
|
+
const {key} = event
|
|
353
|
+
const menuItems = this.dialogEl.querySelectorAll(
|
|
354
|
+
"li.content-menu-item:not(.disabled)"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
const columnsDiv = this.dialogEl.querySelector(".content-menu-columns")
|
|
358
|
+
const totalColumns = columnsDiv
|
|
359
|
+
? columnsDiv.querySelectorAll(".content-menu-list").length
|
|
360
|
+
: 1
|
|
361
|
+
const itemsPerColumn =
|
|
362
|
+
totalColumns > 1
|
|
363
|
+
? Math.ceil(menuItems.length / totalColumns)
|
|
364
|
+
: menuItems.length
|
|
365
|
+
|
|
366
|
+
switch (key) {
|
|
367
|
+
case "Escape":
|
|
368
|
+
this.close()
|
|
369
|
+
break
|
|
370
|
+
case "ArrowUp":
|
|
371
|
+
event.preventDefault()
|
|
372
|
+
this.focusedIndex =
|
|
373
|
+
(this.focusedIndex - 1 + menuItems.length) %
|
|
374
|
+
menuItems.length
|
|
375
|
+
this.focusMenuItem(this.focusedIndex)
|
|
376
|
+
break
|
|
377
|
+
case "ArrowDown":
|
|
378
|
+
event.preventDefault()
|
|
379
|
+
this.focusedIndex = (this.focusedIndex + 1) % menuItems.length
|
|
380
|
+
this.focusMenuItem(this.focusedIndex)
|
|
381
|
+
break
|
|
382
|
+
case "ArrowLeft":
|
|
383
|
+
if (totalColumns <= 1) {
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
event.preventDefault()
|
|
387
|
+
{
|
|
388
|
+
const currentCol = Math.floor(
|
|
389
|
+
this.focusedIndex / itemsPerColumn
|
|
390
|
+
)
|
|
391
|
+
const currentRow = this.focusedIndex % itemsPerColumn
|
|
392
|
+
if (currentCol > 0) {
|
|
393
|
+
let newIndex =
|
|
394
|
+
(currentCol - 1) * itemsPerColumn + currentRow
|
|
395
|
+
const prevColEnd = Math.min(
|
|
396
|
+
currentCol * itemsPerColumn,
|
|
397
|
+
menuItems.length
|
|
398
|
+
)
|
|
399
|
+
if (newIndex >= prevColEnd) {
|
|
400
|
+
newIndex = prevColEnd - 1
|
|
401
|
+
}
|
|
402
|
+
this.focusedIndex = newIndex
|
|
403
|
+
this.focusMenuItem(this.focusedIndex)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
break
|
|
407
|
+
case "ArrowRight":
|
|
408
|
+
if (totalColumns <= 1) {
|
|
409
|
+
break
|
|
410
|
+
}
|
|
411
|
+
event.preventDefault()
|
|
412
|
+
{
|
|
413
|
+
const currentCol = Math.floor(
|
|
414
|
+
this.focusedIndex / itemsPerColumn
|
|
415
|
+
)
|
|
416
|
+
const currentRow = this.focusedIndex % itemsPerColumn
|
|
417
|
+
if (currentCol < totalColumns - 1) {
|
|
418
|
+
let newIndex =
|
|
419
|
+
(currentCol + 1) * itemsPerColumn + currentRow
|
|
420
|
+
const nextColEnd = Math.min(
|
|
421
|
+
(currentCol + 2) * itemsPerColumn,
|
|
422
|
+
menuItems.length
|
|
423
|
+
)
|
|
424
|
+
if (newIndex >= nextColEnd) {
|
|
425
|
+
newIndex = nextColEnd - 1
|
|
426
|
+
}
|
|
427
|
+
this.focusedIndex = newIndex
|
|
428
|
+
this.focusMenuItem(this.focusedIndex)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
break
|
|
432
|
+
case "Tab":
|
|
433
|
+
event.preventDefault()
|
|
434
|
+
if (event.shiftKey) {
|
|
435
|
+
this.focusedIndex =
|
|
436
|
+
(this.focusedIndex - 1 + menuItems.length) %
|
|
437
|
+
menuItems.length
|
|
438
|
+
} else {
|
|
439
|
+
this.focusedIndex =
|
|
440
|
+
(this.focusedIndex + 1) % menuItems.length
|
|
441
|
+
}
|
|
442
|
+
this.focusMenuItem(this.focusedIndex)
|
|
443
|
+
break
|
|
444
|
+
case "Enter":
|
|
445
|
+
case " ": {
|
|
446
|
+
event.preventDefault()
|
|
447
|
+
const menuItem = this.menu.content[this.focusedIndex]
|
|
448
|
+
if (!menuItem.disabled?.(this.page)) {
|
|
449
|
+
menuItem.action(this.page)
|
|
450
|
+
this.close()
|
|
451
|
+
}
|
|
452
|
+
break
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
focusFirstMenuItem() {
|
|
458
|
+
const menuItems = this.dialogEl.querySelectorAll(
|
|
459
|
+
"li.content-menu-item:not(.disabled)"
|
|
460
|
+
)
|
|
461
|
+
if (menuItems.length > 0) {
|
|
462
|
+
this.focusedIndex = 0
|
|
463
|
+
this.focusMenuItem(this.focusedIndex)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
focusMenuItem(index) {
|
|
468
|
+
const menuItems = this.dialogEl.querySelectorAll(
|
|
469
|
+
"li.content-menu-item:not(.disabled)"
|
|
470
|
+
)
|
|
471
|
+
if (menuItems[index]) {
|
|
472
|
+
menuItems[index].focus()
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {keyName} from "w3c-keyname"
|
|
2
|
+
|
|
3
|
+
import {whenReady} from "./basic.js"
|
|
4
|
+
import {ContentMenu} from "./content_menu.js"
|
|
5
|
+
|
|
6
|
+
let bulkId = 0
|
|
7
|
+
|
|
8
|
+
export class DatatableBulk {
|
|
9
|
+
constructor(page, model, checkboxColumn) {
|
|
10
|
+
this.page = page
|
|
11
|
+
this.model = model
|
|
12
|
+
this.checkboxColumn = checkboxColumn
|
|
13
|
+
|
|
14
|
+
this.id = `dt-bulk-${++bulkId}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
init(table) {
|
|
18
|
+
this.table = table
|
|
19
|
+
whenReady().then(() => this.bindEvents())
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
update() {
|
|
23
|
+
this.model.content = this.model.content.sort(
|
|
24
|
+
(a, b) => a.order - b.order
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
bindEvents() {
|
|
29
|
+
// Store the bound functions as instance variables so we can remove them later
|
|
30
|
+
this.boundOnClick = this.onClick.bind(this)
|
|
31
|
+
this.boundOnTableCheckChange = this.onTableCheckChange.bind(this)
|
|
32
|
+
this.boundOnKeyDown = this.onKeyDown.bind(this)
|
|
33
|
+
|
|
34
|
+
this.page.dom.addEventListener("click", this.boundOnClick)
|
|
35
|
+
this.table.dom.addEventListener("change", this.boundOnTableCheckChange)
|
|
36
|
+
this.table.dom.addEventListener("keydown", this.boundOnKeyDown)
|
|
37
|
+
this.onTableCheckChange()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The new destroy() method removes all event listeners that were added and cleans up DOM elements.
|
|
41
|
+
destroy() {
|
|
42
|
+
if (this.page && this.page.dom && this.boundOnClick) {
|
|
43
|
+
this.page.dom.removeEventListener("click", this.boundOnClick)
|
|
44
|
+
}
|
|
45
|
+
if (this.table && this.table.dom) {
|
|
46
|
+
if (this.boundOnTableCheckChange) {
|
|
47
|
+
this.table.dom.removeEventListener(
|
|
48
|
+
"change",
|
|
49
|
+
this.boundOnTableCheckChange
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
if (this.boundOnKeyDown) {
|
|
53
|
+
this.table.dom.removeEventListener(
|
|
54
|
+
"keydown",
|
|
55
|
+
this.boundOnKeyDown
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Remove the bulk element from the DOM if it exists
|
|
61
|
+
const el = document.getElementById(this.id)
|
|
62
|
+
if (el && el.parentNode) {
|
|
63
|
+
el.parentNode.removeChild(el)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Clear any references to help garbage collection
|
|
67
|
+
this.page = null
|
|
68
|
+
this.table = null
|
|
69
|
+
this.model = null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onKeyDown(event) {
|
|
73
|
+
const key = keyName(event)
|
|
74
|
+
const el = this.page.dom.querySelector(`#${this.id}`)
|
|
75
|
+
|
|
76
|
+
if (!el) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (key === "Enter" && this.page.getSelected().length > 0) {
|
|
81
|
+
// Open the content menu when Enter is pressed and at least one row is selected
|
|
82
|
+
event.preventDefault()
|
|
83
|
+
event.stopImmediatePropagation()
|
|
84
|
+
event.stopPropagation()
|
|
85
|
+
|
|
86
|
+
const contentMenu = new ContentMenu({
|
|
87
|
+
menu: this.model,
|
|
88
|
+
width: 280,
|
|
89
|
+
page: this.page,
|
|
90
|
+
menuPos: {
|
|
91
|
+
X: Number.parseInt(el.getBoundingClientRect().left),
|
|
92
|
+
Y: Number.parseInt(el.getBoundingClientRect().bottom)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
contentMenu.open()
|
|
96
|
+
} else if (
|
|
97
|
+
key === " " &&
|
|
98
|
+
event.target === el.querySelector("input[type=checkbox]")
|
|
99
|
+
) {
|
|
100
|
+
// Toggle "Select All" when Space is pressed on the checkbox
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
event.stopImmediatePropagation()
|
|
103
|
+
event.stopPropagation()
|
|
104
|
+
|
|
105
|
+
const isChecked = this.isAllChecked()
|
|
106
|
+
this.toggleSelectAll(!isChecked)
|
|
107
|
+
} else if ((event.ctrlKey || event.metaKey) && key === "a") {
|
|
108
|
+
// Select all when Ctrl+A is pressed
|
|
109
|
+
const isChecked = this.isAllChecked()
|
|
110
|
+
this.toggleSelectAll(!isChecked)
|
|
111
|
+
event.preventDefault()
|
|
112
|
+
event.stopImmediatePropagation()
|
|
113
|
+
event.stopPropagation()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
toggleSelectAll(checked) {
|
|
118
|
+
// Update the DataTable instance
|
|
119
|
+
if (this.table) {
|
|
120
|
+
this.table.data.data.forEach(row => {
|
|
121
|
+
if (row.cells[this.checkboxColumn]) {
|
|
122
|
+
row.cells[this.checkboxColumn].data = checked
|
|
123
|
+
row.cells[this.checkboxColumn].text = String(checked)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
this.table.update()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.onTableCheckChange()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onTableCheckChange() {
|
|
133
|
+
const el = this.page.dom.querySelector(`#${this.id}`)
|
|
134
|
+
if (!el) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const allChecked = this.isAllChecked()
|
|
139
|
+
el.querySelector("input[type=checkbox]").checked = allChecked
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
isAllChecked() {
|
|
143
|
+
const checkboxes = Array.from(
|
|
144
|
+
this.table.dom.querySelectorAll("input.entry-select[type=checkbox]")
|
|
145
|
+
)
|
|
146
|
+
const unchecked = checkboxes.filter(box => !box.checked)
|
|
147
|
+
return !unchecked.length && checkboxes.length
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
onClick(event) {
|
|
151
|
+
const target = event.target
|
|
152
|
+
if (target.matches(`#${this.id} *`)) {
|
|
153
|
+
event.preventDefault()
|
|
154
|
+
event.stopImmediatePropagation()
|
|
155
|
+
event.stopPropagation()
|
|
156
|
+
|
|
157
|
+
if (target.matches(".dt-bulk-dropdown, .dt-bulk-dropdown *")) {
|
|
158
|
+
// Dropdown
|
|
159
|
+
const el = document.querySelector(`#${this.id}`)
|
|
160
|
+
if (el) {
|
|
161
|
+
const contentMenu = new ContentMenu({
|
|
162
|
+
menu: this.model,
|
|
163
|
+
width: 280,
|
|
164
|
+
page: this.page,
|
|
165
|
+
menuPos: {
|
|
166
|
+
X: Number.parseInt(event.pageX),
|
|
167
|
+
Y: Number.parseInt(event.pageY)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
contentMenu.open()
|
|
171
|
+
}
|
|
172
|
+
} else if (
|
|
173
|
+
target.matches(".fw-check + label, .fw-check + label *")
|
|
174
|
+
) {
|
|
175
|
+
// Click on bulk checkbox
|
|
176
|
+
const isChecked = this.isAllChecked()
|
|
177
|
+
this.toggleSelectAll(!isChecked)
|
|
178
|
+
target
|
|
179
|
+
.closest("div.datatable-wrapper")
|
|
180
|
+
.querySelector("input[type=checkbox]").checked = !isChecked
|
|
181
|
+
this.onTableCheckChange()
|
|
182
|
+
}
|
|
183
|
+
} else if (target.matches(".fw-data-table .entry-select + label")) {
|
|
184
|
+
// The browser will try to scroll the checkbox into view and that will break the page layout.
|
|
185
|
+
event.preventDefault()
|
|
186
|
+
event.stopImmediatePropagation()
|
|
187
|
+
event.stopPropagation()
|
|
188
|
+
const tr = target.closest("tr")
|
|
189
|
+
const index = parseInt(tr.dataset.index)
|
|
190
|
+
const row = this.table.data.data[index]
|
|
191
|
+
const cell = row.cells[this.checkboxColumn]
|
|
192
|
+
cell.data = !cell.data
|
|
193
|
+
cell.text = String(cell.data)
|
|
194
|
+
this.table.update()
|
|
195
|
+
this.onTableCheckChange()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getHTML() {
|
|
200
|
+
return `<div id="${this.id}" class="dt-bulk" role="group" aria-label="Bulk actions">
|
|
201
|
+
<input type="checkbox" id="${this.id}_check" class="fw-check" aria-label="Select all">
|
|
202
|
+
<label for="${this.id}_check"></label>
|
|
203
|
+
<span class="dt-bulk-dropdown" tabindex="0" role="button" aria-label="Open bulk actions menu">
|
|
204
|
+
<i class="fa fa-caret-down"></i>
|
|
205
|
+
</span>
|
|
206
|
+
</div>`
|
|
207
|
+
}
|
|
208
|
+
}
|