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.
@@ -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
+ }