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,611 @@
|
|
|
1
|
+
import {DiffDOM} from "diff-dom"
|
|
2
|
+
import {keyName} from "w3c-keyname"
|
|
3
|
+
import {escapeText, whenReady} from "./basic.js"
|
|
4
|
+
|
|
5
|
+
export class OverviewMenuView {
|
|
6
|
+
constructor(overview, model) {
|
|
7
|
+
this.overview = overview
|
|
8
|
+
this.model = model()
|
|
9
|
+
this.dd = new DiffDOM({
|
|
10
|
+
valueDiffing: false
|
|
11
|
+
})
|
|
12
|
+
this.openedMenu = false
|
|
13
|
+
this.listeners = {}
|
|
14
|
+
this.keyboardShortcuts = new Map()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
init() {
|
|
18
|
+
whenReady().then(() => {
|
|
19
|
+
this.addMissingIds(this.model)
|
|
20
|
+
this.bindEvents()
|
|
21
|
+
this.setupKeyboardShortcuts()
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addMissingIds(menu) {
|
|
26
|
+
// Add missing ids to menu items that don't have an ID.
|
|
27
|
+
menu.content.forEach(item => {
|
|
28
|
+
if (!item.id) {
|
|
29
|
+
item.id = Math.random().toString(36).substring(2)
|
|
30
|
+
}
|
|
31
|
+
if (item.type === "dropdown") {
|
|
32
|
+
this.addMissingIds(item)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
bindEvents() {
|
|
38
|
+
this.menuEl = document.getElementById("fw-overview-menu")
|
|
39
|
+
this.listeners.onclick = event => this.onclick(event)
|
|
40
|
+
document.body.addEventListener("click", this.listeners.onclick)
|
|
41
|
+
this.listeners.oninput = event => this.oninput(event)
|
|
42
|
+
document.body.addEventListener("input", this.listeners.oninput)
|
|
43
|
+
this.listeners.onKeydown = event => this.onKeydown(event)
|
|
44
|
+
document.body.addEventListener("keydown", this.listeners.onKeydown)
|
|
45
|
+
this.listeners.onFocus = event => this.onFocus(event)
|
|
46
|
+
document.body.addEventListener("focus", this.listeners.onFocus, true)
|
|
47
|
+
this.update()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setupKeyboardShortcuts() {
|
|
51
|
+
// Map all keyboard shortcuts from the menu model
|
|
52
|
+
this.model.content.forEach(menuItem => {
|
|
53
|
+
if (menuItem.keys) {
|
|
54
|
+
this.keyboardShortcuts.set(
|
|
55
|
+
menuItem.keys.toLowerCase(),
|
|
56
|
+
menuItem
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
onKeydown(event) {
|
|
63
|
+
let name = keyName(event)
|
|
64
|
+
if (event.altKey) {
|
|
65
|
+
name = "alt-" + name.toLowerCase()
|
|
66
|
+
const menuItem = this.keyboardShortcuts.get(name)
|
|
67
|
+
if (menuItem) {
|
|
68
|
+
event.preventDefault()
|
|
69
|
+
event.stopPropagation()
|
|
70
|
+
event.stopImmediatePropagation()
|
|
71
|
+
|
|
72
|
+
if (menuItem.type === "search") {
|
|
73
|
+
const inputEl = document.getElementById(
|
|
74
|
+
`${menuItem.id}-input`
|
|
75
|
+
)
|
|
76
|
+
if (inputEl) {
|
|
77
|
+
inputEl.focus()
|
|
78
|
+
}
|
|
79
|
+
} else if (menuItem.type === "dropdown") {
|
|
80
|
+
// Toggle dropdown menu
|
|
81
|
+
// If the menu is already open, close it
|
|
82
|
+
if (
|
|
83
|
+
this.openedMenu === this.model.content.indexOf(menuItem)
|
|
84
|
+
) {
|
|
85
|
+
menuItem.open = false
|
|
86
|
+
this.openedMenu = false
|
|
87
|
+
this.update()
|
|
88
|
+
} else {
|
|
89
|
+
if (this.openedMenu !== false) {
|
|
90
|
+
this.model.content[this.openedMenu].open = false
|
|
91
|
+
}
|
|
92
|
+
menuItem.open = true
|
|
93
|
+
this.openedMenu = this.model.content.indexOf(menuItem)
|
|
94
|
+
this.update()
|
|
95
|
+
const firstDropdownItem = this.menuEl.querySelector(
|
|
96
|
+
`.fw-pulldown-item.selected`
|
|
97
|
+
)
|
|
98
|
+
if (firstDropdownItem) {
|
|
99
|
+
firstDropdownItem.focus()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else if (menuItem.action) {
|
|
103
|
+
menuItem.action(this.overview)
|
|
104
|
+
}
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Handle horizontal navigation between menu items
|
|
110
|
+
if (name === "ArrowLeft" || name === "ArrowRight") {
|
|
111
|
+
const menuItems = Array.from(
|
|
112
|
+
this.menuEl.querySelectorAll("#fw-overview-menu > li")
|
|
113
|
+
)
|
|
114
|
+
const focusedElement = document.activeElement
|
|
115
|
+
const currentMenuItem = focusedElement.closest("li")
|
|
116
|
+
|
|
117
|
+
if (currentMenuItem) {
|
|
118
|
+
event.preventDefault()
|
|
119
|
+
const currentIndex = menuItems.indexOf(currentMenuItem)
|
|
120
|
+
let newIndex
|
|
121
|
+
|
|
122
|
+
if (name === "ArrowLeft") {
|
|
123
|
+
newIndex =
|
|
124
|
+
currentIndex > 0
|
|
125
|
+
? currentIndex - 1
|
|
126
|
+
: menuItems.length - 1
|
|
127
|
+
} else {
|
|
128
|
+
newIndex =
|
|
129
|
+
currentIndex < menuItems.length - 1
|
|
130
|
+
? currentIndex + 1
|
|
131
|
+
: 0
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const nextMenuItem = menuItems[newIndex].querySelector(
|
|
135
|
+
".fw-dropdown-menu, .fw-text-menu, button, input"
|
|
136
|
+
)
|
|
137
|
+
if (nextMenuItem) {
|
|
138
|
+
nextMenuItem.focus()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Handle Enter and Space to open dropdown menus
|
|
145
|
+
if (name === "Enter" || name === " ") {
|
|
146
|
+
const focusedElement = document.activeElement
|
|
147
|
+
if (focusedElement.matches(".fw-dropdown-menu")) {
|
|
148
|
+
event.preventDefault()
|
|
149
|
+
const menuItem = this.findMenuItemFromElement(focusedElement)
|
|
150
|
+
if (menuItem && menuItem.type === "dropdown") {
|
|
151
|
+
if (this.openedMenu !== false) {
|
|
152
|
+
this.model.content[this.openedMenu].open = false
|
|
153
|
+
}
|
|
154
|
+
menuItem.open = true
|
|
155
|
+
this.openedMenu = this.model.content.indexOf(menuItem)
|
|
156
|
+
menuItem.selectedIndex = 0
|
|
157
|
+
this.update()
|
|
158
|
+
|
|
159
|
+
const firstDropdownItem = this.menuEl.querySelector(
|
|
160
|
+
`.fw-pulldown-item.selected`
|
|
161
|
+
)
|
|
162
|
+
if (firstDropdownItem) {
|
|
163
|
+
firstDropdownItem.focus()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.openedMenu !== false) {
|
|
171
|
+
const menuItem = this.model.content[this.openedMenu]
|
|
172
|
+
|
|
173
|
+
if (menuItem.type === "dropdown") {
|
|
174
|
+
if (name === "ArrowDown" || name === "ArrowUp") {
|
|
175
|
+
event.preventDefault()
|
|
176
|
+
event.stopPropagation()
|
|
177
|
+
|
|
178
|
+
// Find currently selected item
|
|
179
|
+
let selectedIndex = -1
|
|
180
|
+
if (menuItem.selectedIndex !== undefined) {
|
|
181
|
+
selectedIndex = menuItem.selectedIndex
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Calculate new index
|
|
185
|
+
if (name === "ArrowDown") {
|
|
186
|
+
selectedIndex =
|
|
187
|
+
selectedIndex < menuItem.content.length - 1
|
|
188
|
+
? selectedIndex + 1
|
|
189
|
+
: 0
|
|
190
|
+
} else {
|
|
191
|
+
selectedIndex -= 1
|
|
192
|
+
if (selectedIndex < 0) {
|
|
193
|
+
// Close menu
|
|
194
|
+
menuItem.open = false
|
|
195
|
+
this.openedMenu = false
|
|
196
|
+
delete menuItem.selectedIndex
|
|
197
|
+
this.update()
|
|
198
|
+
const dropdownButton = this.menuEl.querySelector(
|
|
199
|
+
`#${menuItem.id}-button`
|
|
200
|
+
)
|
|
201
|
+
if (dropdownButton) {
|
|
202
|
+
dropdownButton.focus()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
menuItem.selectedIndex = selectedIndex
|
|
208
|
+
this.update()
|
|
209
|
+
const selectedEl = this.menuEl.querySelector(
|
|
210
|
+
`.fw-pulldown-item.selected`
|
|
211
|
+
)
|
|
212
|
+
if (selectedEl) {
|
|
213
|
+
selectedEl.focus()
|
|
214
|
+
}
|
|
215
|
+
} else if (name === "Enter" || name === " ") {
|
|
216
|
+
event.preventDefault()
|
|
217
|
+
event.stopPropagation()
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
menuItem.selectedIndex !== undefined &&
|
|
221
|
+
menuItem.content[menuItem.selectedIndex]
|
|
222
|
+
) {
|
|
223
|
+
const selectedItem =
|
|
224
|
+
menuItem.content[menuItem.selectedIndex]
|
|
225
|
+
if (selectedItem.action) {
|
|
226
|
+
selectedItem.action(this.overview)
|
|
227
|
+
menuItem.open = false
|
|
228
|
+
this.openedMenu = false
|
|
229
|
+
delete menuItem.selectedIndex
|
|
230
|
+
this.update()
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (name === "Escape") {
|
|
234
|
+
event.preventDefault()
|
|
235
|
+
event.stopPropagation()
|
|
236
|
+
|
|
237
|
+
menuItem.open = false
|
|
238
|
+
this.openedMenu = false
|
|
239
|
+
delete menuItem.selectedIndex
|
|
240
|
+
this.update()
|
|
241
|
+
const dropdownButton = document.getElementById(
|
|
242
|
+
`${menuItem.id}-button`
|
|
243
|
+
)
|
|
244
|
+
if (dropdownButton) {
|
|
245
|
+
dropdownButton.focus()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
onFocus(event) {
|
|
253
|
+
// Ignore if the focus event is triggered by JavaScript
|
|
254
|
+
if (event.isTrusted === false) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
const target = event.target
|
|
258
|
+
if (this.openedMenu !== false) {
|
|
259
|
+
if (target.matches("#fw-overview-menu li .fw-pulldown-item")) {
|
|
260
|
+
const menuItem = this.model.content[this.openedMenu]
|
|
261
|
+
if (menuItem) {
|
|
262
|
+
const itemNumber = Array.from(
|
|
263
|
+
target.parentElement.parentElement.children
|
|
264
|
+
).indexOf(target.parentElement)
|
|
265
|
+
menuItem.selectedIndex = itemNumber
|
|
266
|
+
this.update()
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// Close dropdown menu if focus is outside of the dropdown
|
|
270
|
+
const menuItem = this.model.content[this.openedMenu]
|
|
271
|
+
if (menuItem) {
|
|
272
|
+
menuItem.open = false
|
|
273
|
+
delete menuItem.selectedIndex
|
|
274
|
+
this.openedMenu = false
|
|
275
|
+
this.update()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
findMenuItemFromElement(element) {
|
|
282
|
+
const menuItem = element.closest("li")
|
|
283
|
+
if (!menuItem) {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let menuNumber = 0
|
|
288
|
+
let seekItem = menuItem
|
|
289
|
+
while (seekItem.previousElementSibling) {
|
|
290
|
+
menuNumber++
|
|
291
|
+
seekItem = seekItem.previousElementSibling
|
|
292
|
+
}
|
|
293
|
+
return this.model.content[menuNumber]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
focusMenuItem(menuItem) {
|
|
297
|
+
const menuEl = this.menuEl.querySelector(`#${menuItem.id}`)
|
|
298
|
+
if (menuEl) {
|
|
299
|
+
menuEl.focus()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
oninput(event) {
|
|
304
|
+
const target = event.target
|
|
305
|
+
if (target.matches("#fw-overview-menu > li > .fw-button > input")) {
|
|
306
|
+
// A text was entered in a top entry. we find which one.
|
|
307
|
+
let menuNumber = 0
|
|
308
|
+
let seekItem = target.closest("li")
|
|
309
|
+
while (seekItem.previousElementSibling) {
|
|
310
|
+
menuNumber++
|
|
311
|
+
seekItem = seekItem.previousElementSibling
|
|
312
|
+
}
|
|
313
|
+
const menuItem = this.model.content[menuNumber]
|
|
314
|
+
if (menuItem.input) {
|
|
315
|
+
menuItem.input(this.overview, target.value)
|
|
316
|
+
target.focus()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
onclick(event) {
|
|
322
|
+
const target = event.target
|
|
323
|
+
if (
|
|
324
|
+
target.matches("#fw-overview-menu li li, #fw-overview-menu li li *")
|
|
325
|
+
) {
|
|
326
|
+
event.preventDefault()
|
|
327
|
+
let itemNumber = 0
|
|
328
|
+
let seekItem = target.closest("li")
|
|
329
|
+
while (seekItem.previousElementSibling) {
|
|
330
|
+
itemNumber++
|
|
331
|
+
seekItem = seekItem.previousElementSibling
|
|
332
|
+
}
|
|
333
|
+
let menuNumber = 0
|
|
334
|
+
seekItem = seekItem.parentElement.parentElement.parentElement
|
|
335
|
+
while (seekItem.previousElementSibling) {
|
|
336
|
+
menuNumber++
|
|
337
|
+
seekItem = seekItem.previousElementSibling
|
|
338
|
+
}
|
|
339
|
+
this.model.content[menuNumber].content[itemNumber].action(
|
|
340
|
+
this.overview
|
|
341
|
+
)
|
|
342
|
+
this.model.content[menuNumber].open = false
|
|
343
|
+
|
|
344
|
+
if (this.model.content[menuNumber].type === "dropdown") {
|
|
345
|
+
this.model.content[menuNumber].title =
|
|
346
|
+
this.model.content[menuNumber].content[itemNumber].title
|
|
347
|
+
this.openedMenu = false
|
|
348
|
+
this.update()
|
|
349
|
+
}
|
|
350
|
+
return false
|
|
351
|
+
} else if (
|
|
352
|
+
target.matches(
|
|
353
|
+
"#fw-overview-menu li .select-action input[type=checkbox]"
|
|
354
|
+
)
|
|
355
|
+
) {
|
|
356
|
+
event.preventDefault()
|
|
357
|
+
event.stopImmediatePropagation()
|
|
358
|
+
event.stopPropagation()
|
|
359
|
+
// A toolbar dropdown menu item was clicked. We just need to
|
|
360
|
+
// find out which one
|
|
361
|
+
let menuNumber = 0
|
|
362
|
+
let seekItem = target.closest("li")
|
|
363
|
+
while (seekItem.previousElementSibling) {
|
|
364
|
+
menuNumber++
|
|
365
|
+
seekItem = seekItem.previousElementSibling
|
|
366
|
+
}
|
|
367
|
+
const menuItem = this.model.content[menuNumber]
|
|
368
|
+
|
|
369
|
+
if (menuItem.checked === true) {
|
|
370
|
+
menuItem.checked = false
|
|
371
|
+
menuItem.uncheckAction(this.overview)
|
|
372
|
+
} else {
|
|
373
|
+
menuItem.checked = true
|
|
374
|
+
menuItem.checkAction(this.overview)
|
|
375
|
+
}
|
|
376
|
+
return true
|
|
377
|
+
} else if (
|
|
378
|
+
target.matches("#fw-overview-menu li, #fw-overview-menu li *")
|
|
379
|
+
) {
|
|
380
|
+
// A toolbar dropdown menu item was clicked. We just need to
|
|
381
|
+
// find out which one
|
|
382
|
+
let menuNumber = 0
|
|
383
|
+
let seekItem = target.closest("li")
|
|
384
|
+
while (seekItem.previousElementSibling) {
|
|
385
|
+
menuNumber++
|
|
386
|
+
seekItem = seekItem.previousElementSibling
|
|
387
|
+
}
|
|
388
|
+
const menuItem = this.model.content[menuNumber]
|
|
389
|
+
// if it is a dropdown menu, open it. Otherwise execute an
|
|
390
|
+
// associated action.
|
|
391
|
+
if (
|
|
392
|
+
["dropdown", "select-action-dropdown"].includes(menuItem.type)
|
|
393
|
+
) {
|
|
394
|
+
event.preventDefault()
|
|
395
|
+
if (this.openedMenu === menuNumber) {
|
|
396
|
+
this.model.content[this.openedMenu].open = false
|
|
397
|
+
this.openedMenu = false
|
|
398
|
+
} else {
|
|
399
|
+
if (this.openedMenu !== false) {
|
|
400
|
+
this.model.content[this.openedMenu].open = false
|
|
401
|
+
}
|
|
402
|
+
menuItem.open = true
|
|
403
|
+
this.openedMenu = menuNumber
|
|
404
|
+
}
|
|
405
|
+
this.update()
|
|
406
|
+
} else if (menuItem.action) {
|
|
407
|
+
event.preventDefault()
|
|
408
|
+
menuItem.action(this.overview)
|
|
409
|
+
this.announceForScreenReader(gettext("Action completed"))
|
|
410
|
+
if (this.openedMenu !== false) {
|
|
411
|
+
this.model.content[this.openedMenu].open = false
|
|
412
|
+
this.openedMenu = false
|
|
413
|
+
}
|
|
414
|
+
this.update()
|
|
415
|
+
}
|
|
416
|
+
return false
|
|
417
|
+
} else if (this.openedMenu !== false) {
|
|
418
|
+
this.model.content[this.openedMenu].open = false
|
|
419
|
+
this.openedMenu = false
|
|
420
|
+
this.update()
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
update() {
|
|
425
|
+
if (!this.menuEl) {
|
|
426
|
+
// page has not yet been loaded. abort
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
const diff = this.dd.diff(this.menuEl, this.getMenuHTML())
|
|
430
|
+
this.dd.apply(this.menuEl, diff)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
getMenuHTML() {
|
|
434
|
+
return `<ul id="fw-overview-menu">${this.model.content
|
|
435
|
+
.map(
|
|
436
|
+
menuItem =>
|
|
437
|
+
`<li class="fw-overview-menu-item${menuItem.id ? ` ${menuItem.id}` : ""} ${menuItem.type}">${this.getMenuItemHTML(
|
|
438
|
+
menuItem
|
|
439
|
+
)}</li>`
|
|
440
|
+
)
|
|
441
|
+
.join("")}</ul>`
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Underline access keys
|
|
445
|
+
getAccessKeyHTML(title, accessKey) {
|
|
446
|
+
if (!accessKey) {
|
|
447
|
+
return escapeText(title)
|
|
448
|
+
}
|
|
449
|
+
const index = title.toLowerCase().indexOf(accessKey.toLowerCase())
|
|
450
|
+
if (index === -1) {
|
|
451
|
+
return escapeText(title)
|
|
452
|
+
}
|
|
453
|
+
return `${escapeText(title.substring(0, index))}<span class="access-key">${escapeText(
|
|
454
|
+
title.charAt(index)
|
|
455
|
+
)}</span>${escapeText(title.substring(index + 1))}`
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
getMenuItemHTML(menuItem) {
|
|
459
|
+
let returnValue
|
|
460
|
+
switch (menuItem.type) {
|
|
461
|
+
case "dropdown":
|
|
462
|
+
returnValue = this.getDropdownHTML(menuItem)
|
|
463
|
+
break
|
|
464
|
+
case "select-action-dropdown":
|
|
465
|
+
returnValue = this.getSelectionActionDropdownHTML(menuItem)
|
|
466
|
+
break
|
|
467
|
+
case "text":
|
|
468
|
+
returnValue = this.getTextHTML(menuItem)
|
|
469
|
+
break
|
|
470
|
+
case "button":
|
|
471
|
+
returnValue = this.getButtonHTML(menuItem)
|
|
472
|
+
break
|
|
473
|
+
case "search":
|
|
474
|
+
returnValue = this.getSearchHTML(menuItem)
|
|
475
|
+
break
|
|
476
|
+
default:
|
|
477
|
+
returnValue = ""
|
|
478
|
+
break
|
|
479
|
+
}
|
|
480
|
+
return returnValue
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
getSelectionActionDropdownHTML(menuItem) {
|
|
484
|
+
return `
|
|
485
|
+
<div class="select-action fw-button fw-light fw-large">
|
|
486
|
+
<input type="checkbox" ${menuItem.checked ? "checked" : ""}>
|
|
487
|
+
<span class="select-action-dropdown"><i class="fa-solid fa-caret-down"></i></span>
|
|
488
|
+
</div>
|
|
489
|
+
${this.getDropdownListHTML(menuItem)}
|
|
490
|
+
`
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
getDropdownHTML(menuItem) {
|
|
494
|
+
const accessKey = menuItem.keys?.split("-")[1]
|
|
495
|
+
return `
|
|
496
|
+
<div class="dropdown fw-dropdown-menu"
|
|
497
|
+
role="button"
|
|
498
|
+
aria-haspopup="true"
|
|
499
|
+
aria-expanded="${menuItem.open ? "true" : "false"}"
|
|
500
|
+
tabindex="0"
|
|
501
|
+
id="${menuItem.id}-button">
|
|
502
|
+
<label id="${menuItem.id}-label">
|
|
503
|
+
${this.getAccessKeyHTML(
|
|
504
|
+
menuItem.title
|
|
505
|
+
? menuItem.title
|
|
506
|
+
: menuItem.content.length
|
|
507
|
+
? menuItem.content[0].title
|
|
508
|
+
: "",
|
|
509
|
+
accessKey
|
|
510
|
+
)}
|
|
511
|
+
</label>
|
|
512
|
+
<span class="dropdown" aria-hidden="true">
|
|
513
|
+
<i class="fa-solid fa-caret-down"></i>
|
|
514
|
+
</span>
|
|
515
|
+
</div>
|
|
516
|
+
${this.getDropdownListHTML(menuItem)}
|
|
517
|
+
`
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
getDropdownListHTML(menuItem) {
|
|
521
|
+
if (menuItem.open) {
|
|
522
|
+
return `<div class="fw-pulldown fw-left"
|
|
523
|
+
role="menu"
|
|
524
|
+
style="display: block;"
|
|
525
|
+
aria-labelledby="${menuItem.id}-button"
|
|
526
|
+
>
|
|
527
|
+
<ul role="presentation">${menuItem.content
|
|
528
|
+
.map((menuOption, index) =>
|
|
529
|
+
this.getDropdownOptionHTML(menuOption, index)
|
|
530
|
+
)
|
|
531
|
+
.join("")}</ul></div>`
|
|
532
|
+
} else {
|
|
533
|
+
return ""
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
getDropdownOptionHTML(menuOption, index) {
|
|
538
|
+
const menuItem = this.model.content[this.openedMenu]
|
|
539
|
+
const isSelected = menuItem.selectedIndex === index
|
|
540
|
+
return `
|
|
541
|
+
<li role="none">
|
|
542
|
+
<span class="fw-pulldown-item${isSelected ? " selected" : ""}"
|
|
543
|
+
role="menuitem" tabindex="0">
|
|
544
|
+
${escapeText(menuOption.title)}
|
|
545
|
+
</span>
|
|
546
|
+
</li>
|
|
547
|
+
`
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
getButtonHTML(menuItem) {
|
|
551
|
+
return `
|
|
552
|
+
<button class="fw-button fw-light fw-large"
|
|
553
|
+
title="${menuItem.title}"
|
|
554
|
+
tabindex="0"
|
|
555
|
+
role="menuitem">
|
|
556
|
+
${menuItem.title}
|
|
557
|
+
${menuItem.icon ? `<i class="fa-solid fa-${menuItem.icon}" aria-hidden="true"></i>` : ""}
|
|
558
|
+
</button>`
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
announceForScreenReader(message) {
|
|
562
|
+
const announcement = document.createElement("div")
|
|
563
|
+
announcement.setAttribute("aria-live", "polite")
|
|
564
|
+
announcement.classList.add("sr-only") // CSS to visually hide but keep available to screen readers
|
|
565
|
+
announcement.textContent = message
|
|
566
|
+
document.body.appendChild(announcement)
|
|
567
|
+
setTimeout(() => announcement.remove(), 1000)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
getTextHTML(menuItem) {
|
|
571
|
+
const accessKey = menuItem.keys?.split("-")[1]
|
|
572
|
+
return `
|
|
573
|
+
<button class="fw-text-menu"
|
|
574
|
+
title="${menuItem.title}${menuItem.keys ? ` (${menuItem.keys})` : ""}"
|
|
575
|
+
>
|
|
576
|
+
${this.getAccessKeyHTML(menuItem.title, accessKey)}
|
|
577
|
+
</button>`
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
getSearchHTML(menuItem) {
|
|
581
|
+
const accessKey = menuItem.keys?.split("-")[1]
|
|
582
|
+
return `
|
|
583
|
+
<div class="fw-button fw-light fw-large disabled fw-search-field-container">
|
|
584
|
+
<label for="${menuItem.id}-input" class="fw-search-label">
|
|
585
|
+
${this.getAccessKeyHTML(menuItem.title, accessKey)}
|
|
586
|
+
</label>
|
|
587
|
+
<input type="search"
|
|
588
|
+
class="fw-search-field"
|
|
589
|
+
id="${menuItem.id}-input"
|
|
590
|
+
aria-description="${gettext("Type to search")}"
|
|
591
|
+
placeholder="${menuItem.title}"
|
|
592
|
+
aria-label="${menuItem.title}"
|
|
593
|
+
>
|
|
594
|
+
${menuItem.icon ? `<i class="fa-solid fa-${menuItem.icon}" aria-hidden="true"></i>` : ""}
|
|
595
|
+
</div>`
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
destroy() {
|
|
599
|
+
// Remove all event listeners
|
|
600
|
+
document.body.removeEventListener("click", this.listeners.onclick)
|
|
601
|
+
document.body.removeEventListener("input", this.listeners.oninput)
|
|
602
|
+
document.body.removeEventListener("keydown", this.listeners.onKeydown)
|
|
603
|
+
document.body.removeEventListener("focus", this.listeners.onFocus)
|
|
604
|
+
|
|
605
|
+
// Clear references
|
|
606
|
+
this.listeners = {}
|
|
607
|
+
this.keyboardShortcuts.clear()
|
|
608
|
+
this.menuEl = null
|
|
609
|
+
this.openedMenu = false
|
|
610
|
+
}
|
|
611
|
+
}
|
package/src/settings.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
let _settings = null
|
|
2
|
+
|
|
3
|
+
export function initSettings(rawSettings) {
|
|
4
|
+
if (_settings) {
|
|
5
|
+
throw new Error("Settings already initialized")
|
|
6
|
+
}
|
|
7
|
+
// Freeze to prevent accidental mutation at runtime
|
|
8
|
+
_settings = Object.freeze({...rawSettings})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getSettings() {
|
|
12
|
+
if (!_settings) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
"App settings not initialized. Call initSettings() first."
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
return _settings
|
|
18
|
+
}
|
package/src/templates.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {avatarTemplate} from "./user.js"
|
|
2
|
+
import {filterPrimaryEmail} from "./user_util.js"
|
|
3
|
+
|
|
4
|
+
export const baseBodyTemplate = ({user, contents, hasOverview, app}) => `
|
|
5
|
+
<div id="wait">
|
|
6
|
+
<i class="fa fa-spinner fa-pulse"></i>
|
|
7
|
+
</div>
|
|
8
|
+
<header class="fw-header" role="banner">
|
|
9
|
+
<div class="fw-container">
|
|
10
|
+
<a href="${app && app.routes[""].app === "document" ? "/" : "/documents/"}">
|
|
11
|
+
<h1 class="fw-logo">
|
|
12
|
+
<span class="fw-logo-text"></span>
|
|
13
|
+
<img src="${staticUrl("svg/icon.svg")}" alt="Logo" />
|
|
14
|
+
</h1>
|
|
15
|
+
</a>
|
|
16
|
+
<nav id="header-nav" role="navigation" aria-label="${gettext("Site navigation")}"></nav>
|
|
17
|
+
<div id="user-preferences" class="fw-user-preferences fw-header-text">
|
|
18
|
+
<div id="preferences-btn" class="fw-button">
|
|
19
|
+
${avatarTemplate({user})}
|
|
20
|
+
</div>
|
|
21
|
+
<div id="user-preferences-pulldown" class="fw-pulldown fw-right">
|
|
22
|
+
<div data-value="profile">
|
|
23
|
+
<span class='fw-avatar-card'>
|
|
24
|
+
<span class='fw-avatar-card-avatar'>${avatarTemplate({user})}</span>
|
|
25
|
+
<span class='fw-avatar-card-name'>
|
|
26
|
+
${user.username}
|
|
27
|
+
<span class='fw-avatar-card-email'>${filterPrimaryEmail(user.emails)}</span>
|
|
28
|
+
</span>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div data-value="contacts">${gettext("Contacts")}</div>
|
|
32
|
+
<div data-value="logout">${gettext("Log out")}</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div><!-- end user preference -->
|
|
35
|
+
</div><!-- end container -->
|
|
36
|
+
</header>
|
|
37
|
+
<div class="fw-contents-outer">
|
|
38
|
+
${hasOverview ? '<div class="fw-overview-menu-wrapper"><ul id="fw-overview-menu"></ul></div>' : ""}
|
|
39
|
+
<div class="fw-contents">
|
|
40
|
+
${contents}
|
|
41
|
+
</div>
|
|
42
|
+
</div>`
|