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
package/src/dialog.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import {keyName} from "w3c-keyname"
|
|
2
|
+
|
|
3
|
+
import {findTarget} from "./basic.js"
|
|
4
|
+
|
|
5
|
+
const dialogTemplate = ({
|
|
6
|
+
id,
|
|
7
|
+
classes,
|
|
8
|
+
title,
|
|
9
|
+
height,
|
|
10
|
+
width,
|
|
11
|
+
icon,
|
|
12
|
+
buttons,
|
|
13
|
+
zIndex,
|
|
14
|
+
body,
|
|
15
|
+
scroll,
|
|
16
|
+
help,
|
|
17
|
+
canClose,
|
|
18
|
+
note,
|
|
19
|
+
blur
|
|
20
|
+
}) =>
|
|
21
|
+
`<div tabindex="-1" role="dialog"
|
|
22
|
+
class="ui-dialog ui-corner-all ui-widget ui-widget-content ui-front ui-dialog-buttons"
|
|
23
|
+
${id ? `aria-describedby="${id}"` : ""} style="z-index: ${zIndex};">
|
|
24
|
+
<div class="ui-dialog-titlebar ui-corner-all ui-widget-header ui-helper-clearfix">
|
|
25
|
+
${icon ? `<i class="fa fa-${icon}" aria-hidden="true"></i>` : ""}
|
|
26
|
+
<span id="ui-id-2" class="ui-dialog-title">${title}</span>
|
|
27
|
+
${
|
|
28
|
+
help
|
|
29
|
+
? `<button type="button" class="ui-button ui-corner-all ui-widget ui-button-icon-only ui-dialog-titlebar-help" title="${gettext("Help")}">
|
|
30
|
+
<span class="ui-button-icon ui-icon ui-icon-help"> </span>
|
|
31
|
+
<span class="ui-button-icon-space"> </span>
|
|
32
|
+
${gettext("Help")}
|
|
33
|
+
</button>`
|
|
34
|
+
: ""
|
|
35
|
+
}
|
|
36
|
+
${
|
|
37
|
+
canClose
|
|
38
|
+
? `<button type="button" class="ui-button ui-corner-all ui-widget ui-button-icon-only ui-dialog-titlebar-close" title="${gettext("Close")}">
|
|
39
|
+
<span class="ui-button-icon ui-icon ui-icon-closethick"> </span>
|
|
40
|
+
<span class="ui-button-icon-space"> </span>
|
|
41
|
+
${gettext("Close")}
|
|
42
|
+
</button>`
|
|
43
|
+
: ""
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
<div ${id ? `id="${id}"` : ""} class="ui-dialog-content ui-widget-content${classes ? ` ${classes}` : ""}${scroll ? " ui-scrollable" : ""}" style="width: ${width}; height: ${height};">
|
|
48
|
+
${note.text ? `<div class="note-container">${noteTemplate(note)}</div>` : ""}
|
|
49
|
+
${body}
|
|
50
|
+
</div>
|
|
51
|
+
<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix">
|
|
52
|
+
<div class="ui-dialog-buttonset">${buttonsTemplate({buttons})}</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="ui-widget-overlay ui-front${blur === false ? " no-blur" : ""}" style="z-index: ${zIndex - 1}"></div>`
|
|
56
|
+
|
|
57
|
+
const noteTemplate = note => {
|
|
58
|
+
return note.text
|
|
59
|
+
? `<p class="noteEl ${note.display ? "" : "hide"}">${note.text}</p>`
|
|
60
|
+
: ""
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const buttonsTemplate = ({buttons}) =>
|
|
64
|
+
buttons.map(button => buttonTemplate(button)).join("")
|
|
65
|
+
|
|
66
|
+
const buttonTemplate = ({
|
|
67
|
+
text,
|
|
68
|
+
classes,
|
|
69
|
+
icon,
|
|
70
|
+
dropdown
|
|
71
|
+
}) => `<button type="button" class="${classes ? classes : "fw-light"} fw-button ui-button ui-corner-all ui-widget">
|
|
72
|
+
${icon ? `<i class="fa fa-${icon}" aria-hidden="true"></i>` : ""}
|
|
73
|
+
${text}
|
|
74
|
+
${dropdown ? '<i class="fa fa-caret-down" aria-hidden="true"></i>' : ""}
|
|
75
|
+
</button>`
|
|
76
|
+
|
|
77
|
+
const BUTTON_TYPES = {
|
|
78
|
+
close: {
|
|
79
|
+
text: gettext("Close"),
|
|
80
|
+
classes: "fw-orange",
|
|
81
|
+
click: dialog => () => dialog.close()
|
|
82
|
+
},
|
|
83
|
+
cancel: {
|
|
84
|
+
text: gettext("Cancel"),
|
|
85
|
+
classes: "fw-orange",
|
|
86
|
+
click: dialog => () => dialog.close()
|
|
87
|
+
},
|
|
88
|
+
ok: {
|
|
89
|
+
text: gettext("OK"),
|
|
90
|
+
classes: "fw-dark",
|
|
91
|
+
click: dialog => () => dialog.close()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class Dialog {
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.id = options.id || false
|
|
98
|
+
this.classes = options.classes || false
|
|
99
|
+
this.title = options.title || ""
|
|
100
|
+
this.body = options.body || ""
|
|
101
|
+
this.restoreActiveElement = options.restoreActiveElement !== false // default is true
|
|
102
|
+
this.height = options.height ? `${options.height}px` : "auto"
|
|
103
|
+
this.width = options.width ? `${options.width}px` : "auto"
|
|
104
|
+
this.canClose = "canClose" in options ? options.canClose : true
|
|
105
|
+
this.help = "help" in options ? options.help : false
|
|
106
|
+
this.note = "note" in options ? options.note : {}
|
|
107
|
+
this.blur = "blur" in options ? options.blur : true
|
|
108
|
+
this.buttons = []
|
|
109
|
+
if (options.buttons) {
|
|
110
|
+
this.setButtons(options.buttons)
|
|
111
|
+
}
|
|
112
|
+
this.beforeClose = options.beforeClose || false
|
|
113
|
+
this.onClose = options.onClose || false
|
|
114
|
+
this.icon = options.icon || false
|
|
115
|
+
this.scroll = options.scroll || false
|
|
116
|
+
this.canEscape =
|
|
117
|
+
options.canEscape ??
|
|
118
|
+
options.buttons?.find(button =>
|
|
119
|
+
["cancel", "close"].includes(button.type)
|
|
120
|
+
) ??
|
|
121
|
+
false
|
|
122
|
+
this.dialogEl = false
|
|
123
|
+
this.backdropEl = false
|
|
124
|
+
this.dragging = false
|
|
125
|
+
this.hasBeenMoved = false
|
|
126
|
+
this.listeners = {}
|
|
127
|
+
this.fullScreen = options.fullScreen ? options.fullScreen : false
|
|
128
|
+
this.initialFocus = options.initialFocus || null
|
|
129
|
+
|
|
130
|
+
this.previousActiveElement = null // Store previously focused element
|
|
131
|
+
this.firstFocusableEl = null
|
|
132
|
+
this.lastFocusableEl = null
|
|
133
|
+
this.focusableEls = null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setButtons(buttons) {
|
|
137
|
+
this.buttons = buttons.map(button => ({
|
|
138
|
+
text: button.text
|
|
139
|
+
? button.text
|
|
140
|
+
: button.type
|
|
141
|
+
? BUTTON_TYPES[button.type].text
|
|
142
|
+
: "",
|
|
143
|
+
classes: button.classes
|
|
144
|
+
? button.classes
|
|
145
|
+
: button.type
|
|
146
|
+
? BUTTON_TYPES[button.type].classes
|
|
147
|
+
: false,
|
|
148
|
+
click: button.click
|
|
149
|
+
? button.click
|
|
150
|
+
: button.type
|
|
151
|
+
? BUTTON_TYPES[button.type].click(this)
|
|
152
|
+
: "",
|
|
153
|
+
icon: button.icon ? button.icon : false,
|
|
154
|
+
dropdown: button.dropdown ? true : false
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
open() {
|
|
159
|
+
if (this.dialogEl) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Store currently focused element to restore later
|
|
164
|
+
this.previousActiveElement = this.restoreActiveElement
|
|
165
|
+
? document.activeElement
|
|
166
|
+
: null
|
|
167
|
+
|
|
168
|
+
if (this.fullScreen) {
|
|
169
|
+
this.height = "85vh"
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
document.body.insertAdjacentHTML(
|
|
173
|
+
"beforeend",
|
|
174
|
+
dialogTemplate({
|
|
175
|
+
id: this.id,
|
|
176
|
+
classes: this.classes,
|
|
177
|
+
title: this.title,
|
|
178
|
+
height: this.height,
|
|
179
|
+
width: this.width,
|
|
180
|
+
icon: this.icon,
|
|
181
|
+
buttons: this.buttons,
|
|
182
|
+
zIndex: this.nextDialogZIndex(),
|
|
183
|
+
body: this.body,
|
|
184
|
+
scroll: this.scroll,
|
|
185
|
+
canClose: this.canClose,
|
|
186
|
+
help: this.help,
|
|
187
|
+
note: this.note,
|
|
188
|
+
blur: this.blur
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
this.backdropEl = document.body.lastElementChild
|
|
192
|
+
this.dialogEl = this.backdropEl.previousElementSibling
|
|
193
|
+
if (this.fullScreen) {
|
|
194
|
+
this.dialogEl.style.width = "98%"
|
|
195
|
+
this.dialogEl.style.height = "100%"
|
|
196
|
+
this.dialogEl.style.position = "fixed"
|
|
197
|
+
this.dialogEl.style.top = "0px"
|
|
198
|
+
} else {
|
|
199
|
+
this.centerDialog()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Set dialog attributes for accessibility
|
|
203
|
+
this.dialogEl.setAttribute("role", "dialog")
|
|
204
|
+
this.dialogEl.setAttribute("aria-modal", "true")
|
|
205
|
+
if (this.title) {
|
|
206
|
+
this.dialogEl.setAttribute("aria-labelledby", "dialog-title")
|
|
207
|
+
this.dialogEl.querySelector(".ui-dialog-title").id = "dialog-title"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get all focusable elements
|
|
211
|
+
this.focusableEls = this.getFocusableElements()
|
|
212
|
+
this.firstFocusableEl = this.focusableEls[0]
|
|
213
|
+
this.lastFocusableEl = this.focusableEls[this.focusableEls.length - 1]
|
|
214
|
+
|
|
215
|
+
// Set initial focus to the most appropriate element
|
|
216
|
+
const initialFocusElement = this.getInitialFocusElement()
|
|
217
|
+
if (initialFocusElement) {
|
|
218
|
+
setTimeout(() => initialFocusElement.focus(), 0)
|
|
219
|
+
} else if (this.firstFocusableEl) {
|
|
220
|
+
this.firstFocusableEl.focus()
|
|
221
|
+
} else {
|
|
222
|
+
this.dialogEl.focus()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.bind()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
refreshButtons() {
|
|
229
|
+
this.dialogEl.querySelector(".ui-dialog-buttonset").innerHTML =
|
|
230
|
+
buttonsTemplate({buttons: this.buttons})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
refreshNote() {
|
|
234
|
+
this.dialogEl.querySelector(".note-container").innerHTML = noteTemplate(
|
|
235
|
+
this.note
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
centerDialog() {
|
|
240
|
+
const totalWidth = window.innerWidth,
|
|
241
|
+
totalHeight = window.innerHeight,
|
|
242
|
+
dialogWidth = this.dialogEl.clientWidth,
|
|
243
|
+
dialogHeight = this.dialogEl.clientHeight,
|
|
244
|
+
scrollTopOffset = window.pageYOffset,
|
|
245
|
+
scrollLeftOffset = window.pageXOffset
|
|
246
|
+
|
|
247
|
+
this.dialogEl.style.top = `${(totalHeight - dialogHeight) / 2 + scrollTopOffset}px`
|
|
248
|
+
this.dialogEl.style.left = `${(totalWidth - dialogWidth) / 2 + scrollLeftOffset}px`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
adjustDialogToScroll() {
|
|
252
|
+
this.dialogEl.style.top = `${Math.max(
|
|
253
|
+
Math.min(
|
|
254
|
+
this.dialogEl.offsetTop,
|
|
255
|
+
this.backdropEl.scrollHeight -
|
|
256
|
+
this.dialogEl.scrollHeight +
|
|
257
|
+
window.pageYOffset
|
|
258
|
+
),
|
|
259
|
+
window.pageYOffset
|
|
260
|
+
)}px`
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
moveDialog(x, y) {
|
|
264
|
+
this.dialogEl.style.top = `${Math.min(
|
|
265
|
+
Math.max(y - this.dragging.y, 0),
|
|
266
|
+
this.backdropEl.scrollHeight -
|
|
267
|
+
this.dialogEl.scrollHeight +
|
|
268
|
+
window.pageYOffset
|
|
269
|
+
)}px`
|
|
270
|
+
this.dialogEl.style.left = `${Math.min(
|
|
271
|
+
Math.max(x - this.dragging.x, 0),
|
|
272
|
+
document.body.scrollWidth - this.dialogEl.scrollWidth
|
|
273
|
+
)}px`
|
|
274
|
+
this.hasBeenMoved = true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
onScroll(_event) {
|
|
278
|
+
if (this.hasBeenMoved) {
|
|
279
|
+
// The dialog has been moved manually. We just adjust the position to make it stay in the view.
|
|
280
|
+
this.adjustDialogToScroll()
|
|
281
|
+
} else {
|
|
282
|
+
this.centerDialog()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
onKeydown(event) {
|
|
287
|
+
let name = keyName(event)
|
|
288
|
+
if (event.altKey) {
|
|
289
|
+
name = "Alt-" + name
|
|
290
|
+
}
|
|
291
|
+
if (event.ctrlKey) {
|
|
292
|
+
name = "Ctrl-" + name
|
|
293
|
+
}
|
|
294
|
+
if (event.metaKey) {
|
|
295
|
+
name = "Meta-" + name
|
|
296
|
+
}
|
|
297
|
+
if (event.shiftKey) {
|
|
298
|
+
name = "Shift-" + name
|
|
299
|
+
}
|
|
300
|
+
if (name === "Escape" && this.canEscape) {
|
|
301
|
+
event.preventDefault()
|
|
302
|
+
this.close()
|
|
303
|
+
return
|
|
304
|
+
} else if (name === "Tab") {
|
|
305
|
+
if (document.activeElement === this.lastFocusableEl) {
|
|
306
|
+
event.preventDefault()
|
|
307
|
+
this.firstFocusableEl.focus()
|
|
308
|
+
}
|
|
309
|
+
} else if (name === "Shift-Tab") {
|
|
310
|
+
if (document.activeElement === this.firstFocusableEl) {
|
|
311
|
+
event.preventDefault()
|
|
312
|
+
this.lastFocusableEl.focus()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
bind() {
|
|
318
|
+
this.listeners.onKeydown = event => this.onKeydown(event)
|
|
319
|
+
document.body.addEventListener("keydown", this.listeners.onKeydown)
|
|
320
|
+
this.dialogEl.addEventListener("click", event => {
|
|
321
|
+
const el = {}
|
|
322
|
+
switch (true) {
|
|
323
|
+
case findTarget(event, ".ui-dialog-buttonpane button", el): {
|
|
324
|
+
event.preventDefault()
|
|
325
|
+
let buttonNumber = 0
|
|
326
|
+
let seekItem = el.target
|
|
327
|
+
while (seekItem.previousElementSibling) {
|
|
328
|
+
buttonNumber++
|
|
329
|
+
seekItem = seekItem.previousElementSibling
|
|
330
|
+
}
|
|
331
|
+
this.buttons[buttonNumber].click(event)
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
case findTarget(event, ".ui-dialog-titlebar-close", el):
|
|
335
|
+
event.preventDefault()
|
|
336
|
+
this.close()
|
|
337
|
+
break
|
|
338
|
+
case findTarget(event, ".ui-dialog-titlebar-help", el):
|
|
339
|
+
event.preventDefault()
|
|
340
|
+
this.help()
|
|
341
|
+
break
|
|
342
|
+
default:
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
if (!this.fullScreen) {
|
|
347
|
+
this.listeners.onScroll = event => this.onScroll(event)
|
|
348
|
+
window.addEventListener("scroll", this.listeners.onScroll, false)
|
|
349
|
+
this.dialogEl.addEventListener("mousedown", event => {
|
|
350
|
+
const el = {}
|
|
351
|
+
switch (true) {
|
|
352
|
+
case findTarget(event, ".ui-dialog-titlebar", el):
|
|
353
|
+
this.dragging = {
|
|
354
|
+
x: event.clientX - this.dialogEl.offsetLeft,
|
|
355
|
+
y: event.clientY - this.dialogEl.offsetTop
|
|
356
|
+
}
|
|
357
|
+
break
|
|
358
|
+
default:
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
this.dialogEl.addEventListener("mouseup", event => {
|
|
363
|
+
const el = {}
|
|
364
|
+
switch (true) {
|
|
365
|
+
case findTarget(event, ".ui-dialog-titlebar", el):
|
|
366
|
+
this.dragging = false
|
|
367
|
+
break
|
|
368
|
+
default:
|
|
369
|
+
break
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
this.dialogEl.addEventListener("mousemove", event => {
|
|
373
|
+
if (!this.dragging) {
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
this.moveDialog(event.clientX, event.clientY)
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Prevent clicks outside dialog from moving focus outside
|
|
381
|
+
this.backdropEl.addEventListener("click", event => {
|
|
382
|
+
event.preventDefault()
|
|
383
|
+
if (this.canClose) {
|
|
384
|
+
this.close()
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
// Prevent focus from leaving dialog when clicking backdrop
|
|
389
|
+
this.backdropEl.addEventListener("mousedown", event => {
|
|
390
|
+
event.preventDefault()
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
nextDialogZIndex() {
|
|
395
|
+
let zIndex = 100
|
|
396
|
+
document
|
|
397
|
+
.querySelectorAll("div.ui-dialog")
|
|
398
|
+
.forEach(
|
|
399
|
+
dialogEl => (zIndex = Math.max(zIndex, dialogEl.style.zIndex))
|
|
400
|
+
)
|
|
401
|
+
zIndex += 2
|
|
402
|
+
document.body.style.setProperty("--highest-dialog-z-index", zIndex)
|
|
403
|
+
return zIndex
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getFocusableElements() {
|
|
407
|
+
// Get all focusable elements
|
|
408
|
+
const focusableSelectors = [
|
|
409
|
+
"button:not([disabled])",
|
|
410
|
+
"[href]",
|
|
411
|
+
"input:not([disabled])",
|
|
412
|
+
"select:not([disabled])",
|
|
413
|
+
"textarea:not([disabled])",
|
|
414
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
415
|
+
].join(",")
|
|
416
|
+
|
|
417
|
+
const elements = Array.from(
|
|
418
|
+
this.dialogEl.querySelectorAll(focusableSelectors)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
// Filter out hidden elements
|
|
422
|
+
return elements.filter(el => {
|
|
423
|
+
const style = window.getComputedStyle(el)
|
|
424
|
+
return style.display !== "none" && style.visibility !== "hidden"
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
getInitialFocusElement() {
|
|
429
|
+
if (this.initialFocus) {
|
|
430
|
+
const customFocusElement = this.dialogEl.querySelector(
|
|
431
|
+
this.initialFocus
|
|
432
|
+
)
|
|
433
|
+
if (customFocusElement) {
|
|
434
|
+
return customFocusElement
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Get all focusable elements
|
|
438
|
+
const elements = this.getFocusableElements()
|
|
439
|
+
|
|
440
|
+
// Try to find the most appropriate initial focus target
|
|
441
|
+
const priorityElements = [
|
|
442
|
+
// First try to find a text input
|
|
443
|
+
elements.find(el => el.tagName === "INPUT" && el.type === "text"),
|
|
444
|
+
// Then try to find the first button in the button pane
|
|
445
|
+
elements.find(el => el.closest(".ui-dialog-buttonpane")),
|
|
446
|
+
// Then try to find any input
|
|
447
|
+
elements.find(el => el.tagName === "INPUT"),
|
|
448
|
+
// Then try to find any button except close/help
|
|
449
|
+
elements.find(
|
|
450
|
+
el =>
|
|
451
|
+
el.tagName === "BUTTON" &&
|
|
452
|
+
!el.classList.contains("ui-dialog-titlebar-close") &&
|
|
453
|
+
!el.classList.contains("ui-dialog-titlebar-help")
|
|
454
|
+
)
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
// Return the first element that exists
|
|
458
|
+
return priorityElements.find(el => el) || elements[0]
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
close() {
|
|
462
|
+
if (!this.dialogEl) {
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if (!this.fullScreen) {
|
|
466
|
+
window.removeEventListener("scroll", this.listeners.onScroll, false)
|
|
467
|
+
}
|
|
468
|
+
document.body.removeEventListener("keydown", this.listeners.onKeydown)
|
|
469
|
+
if (this.beforeClose) {
|
|
470
|
+
this.beforeClose()
|
|
471
|
+
}
|
|
472
|
+
this.dialogEl.parentElement.removeChild(this.dialogEl)
|
|
473
|
+
this.backdropEl.parentElement.removeChild(this.backdropEl)
|
|
474
|
+
|
|
475
|
+
// Restore focus to previous element
|
|
476
|
+
if (this.previousActiveElement && this.previousActiveElement.focus) {
|
|
477
|
+
this.previousActiveElement.focus()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (this.onClose) {
|
|
481
|
+
this.onClose()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
package/src/events.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {escapeText} from "./basic.js"
|
|
2
|
+
import {Dialog} from "./dialog.js"
|
|
3
|
+
import {ensureCSS} from "./network.js"
|
|
4
|
+
|
|
5
|
+
const faqTemplate = ({escapedQuestions}) =>
|
|
6
|
+
`<div class="faq">
|
|
7
|
+
<ol class="faq-list">
|
|
8
|
+
${escapedQuestions
|
|
9
|
+
.map(
|
|
10
|
+
question => `<li class="faq-item">
|
|
11
|
+
<div>
|
|
12
|
+
<div class="faq-question fw-button fw-light"><i class="fa-solid fa-plus-circle"></i>${question[0]}</div>
|
|
13
|
+
<div class="faq-answer" style="display: none;">${question[1]}</div>
|
|
14
|
+
</div>
|
|
15
|
+
</li>`
|
|
16
|
+
)
|
|
17
|
+
.join("")}
|
|
18
|
+
</ol>
|
|
19
|
+
</div>`
|
|
20
|
+
|
|
21
|
+
export class faqDialog {
|
|
22
|
+
constructor({title = "", questions = []}) {
|
|
23
|
+
ensureCSS(staticUrl("css/faq_dialog.css"))
|
|
24
|
+
const escapedQuestions = []
|
|
25
|
+
|
|
26
|
+
questions.forEach(q => {
|
|
27
|
+
const question = escapeText(q[0])
|
|
28
|
+
let answer
|
|
29
|
+
q[1] = escapeText(q[1])
|
|
30
|
+
if (q[q.length - 1].hasImage) {
|
|
31
|
+
answer = interpolate(...q.slice(1, q.length), true)
|
|
32
|
+
} else {
|
|
33
|
+
answer = q[1]
|
|
34
|
+
}
|
|
35
|
+
escapedQuestions.push([question, answer])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
this.faqDialog = new Dialog({
|
|
39
|
+
title: title,
|
|
40
|
+
body: faqTemplate({escapedQuestions}),
|
|
41
|
+
height: 600,
|
|
42
|
+
width: 900,
|
|
43
|
+
buttons: []
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
open() {
|
|
48
|
+
this.faqDialog.open()
|
|
49
|
+
this.faqDialog.dialogEl
|
|
50
|
+
.querySelectorAll(".faq-question")
|
|
51
|
+
.forEach(element => {
|
|
52
|
+
element.addEventListener("click", () => {
|
|
53
|
+
const iconEle = element.firstElementChild
|
|
54
|
+
const answerEle = element.nextElementSibling
|
|
55
|
+
if (answerEle.style.display == "") {
|
|
56
|
+
iconEle.classList.remove("fa-minus-circle")
|
|
57
|
+
iconEle.classList.add("fa-plus-circle")
|
|
58
|
+
answerEle.style.display = "none"
|
|
59
|
+
} else if (answerEle.style.display == "none") {
|
|
60
|
+
iconEle.classList.remove("fa-plus-circle")
|
|
61
|
+
iconEle.classList.add("fa-minus-circle")
|
|
62
|
+
answerEle.style.display = ""
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {Dialog} from "../dialog.js"
|
|
2
|
+
import {FileSelector} from "./selector.js"
|
|
3
|
+
import {addAlert} from "../basic.js"
|
|
4
|
+
import {NewFolderDialog} from "./new_folder_dialog.js"
|
|
5
|
+
import {moveTemplate} from "./templates.js"
|
|
6
|
+
import {moveFile, shortFileTitle} from "./tools.js"
|
|
7
|
+
/**
|
|
8
|
+
* Functions for the document move dialog.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class FileDialog {
|
|
12
|
+
constructor({
|
|
13
|
+
title = "", // Dialog title
|
|
14
|
+
movingFiles = [], // Array of all files that are to be moved.
|
|
15
|
+
allFiles = [], // Array of all existing files.
|
|
16
|
+
moveUrl = "", // URL to use for moving files
|
|
17
|
+
successMessage = "", // Message for success
|
|
18
|
+
errorMessage = "", // Message for failure
|
|
19
|
+
succcessCallback = (_file, _path) => {}, // Callback on success
|
|
20
|
+
fileIcon = "far fa-file-alt"
|
|
21
|
+
}) {
|
|
22
|
+
this.title = title
|
|
23
|
+
this.movingFiles = movingFiles
|
|
24
|
+
this.allFiles = allFiles
|
|
25
|
+
this.moveUrl = moveUrl
|
|
26
|
+
this.successMessage = successMessage
|
|
27
|
+
this.errorMessage = errorMessage
|
|
28
|
+
this.succcessCallback = succcessCallback
|
|
29
|
+
this.fileIcon = fileIcon
|
|
30
|
+
|
|
31
|
+
this.path = this.getPath()
|
|
32
|
+
this.fileSelector = false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getPath() {
|
|
36
|
+
if (this.movingFiles.length === 1) {
|
|
37
|
+
let path = this.movingFiles[0].path
|
|
38
|
+
if (path.endsWith("/")) {
|
|
39
|
+
path +=
|
|
40
|
+
this.movingFiles[0].title.replace(/\//g, "") ||
|
|
41
|
+
gettext("Untitled")
|
|
42
|
+
}
|
|
43
|
+
return path
|
|
44
|
+
}
|
|
45
|
+
// We are moving several files. We assume they are all in the same directory
|
|
46
|
+
// so we only need to take the file of the first file.
|
|
47
|
+
return this.movingFiles[0].path.split("/").slice(0, -1).join("/") + "/"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
updatePathDir(path) {
|
|
51
|
+
const fileName = this.dialog.dialogEl
|
|
52
|
+
.querySelector("#path")
|
|
53
|
+
.value.split("/")
|
|
54
|
+
.pop()
|
|
55
|
+
this.dialog.dialogEl.querySelector("#path").value = path + fileName
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
init() {
|
|
59
|
+
this.dialog = new Dialog({
|
|
60
|
+
title: this.title,
|
|
61
|
+
id: "move-dialog",
|
|
62
|
+
width: 820,
|
|
63
|
+
height: 440,
|
|
64
|
+
body: moveTemplate({
|
|
65
|
+
path: this.path
|
|
66
|
+
}),
|
|
67
|
+
buttons: [
|
|
68
|
+
{
|
|
69
|
+
text: gettext("New folder"),
|
|
70
|
+
classes: "fw-dark",
|
|
71
|
+
click: () => {
|
|
72
|
+
const dialog = new NewFolderDialog(folderName => {
|
|
73
|
+
if (!this.fileSelector) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
this.fileSelector.addFolder(folderName)
|
|
77
|
+
})
|
|
78
|
+
dialog.open()
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{type: "cancel"},
|
|
82
|
+
{
|
|
83
|
+
text: gettext("Submit"),
|
|
84
|
+
classes: "fw-dark",
|
|
85
|
+
click: () => {
|
|
86
|
+
//apply the current state to server
|
|
87
|
+
let path =
|
|
88
|
+
this.dialog.dialogEl.querySelector("#path").value
|
|
89
|
+
this.dialog.close()
|
|
90
|
+
|
|
91
|
+
if (path === this.path) {
|
|
92
|
+
// No change
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (this.movingFiles.length > 1) {
|
|
96
|
+
if (!path.endsWith("/")) {
|
|
97
|
+
path += "/"
|
|
98
|
+
}
|
|
99
|
+
this.movingFiles.forEach(doc => {
|
|
100
|
+
this.moveFile(
|
|
101
|
+
doc,
|
|
102
|
+
doc.path.endsWith("/")
|
|
103
|
+
? path
|
|
104
|
+
: path + doc.path.split("/").pop()
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
} else {
|
|
108
|
+
this.moveFile(this.movingFiles[0], path)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
})
|
|
114
|
+
this.dialog.open()
|
|
115
|
+
|
|
116
|
+
this.fileSelector = new FileSelector({
|
|
117
|
+
dom: this.dialog.dialogEl.querySelector(".file-selector"),
|
|
118
|
+
files: this.allFiles,
|
|
119
|
+
showFiles: false,
|
|
120
|
+
selectDir: path => this.updatePathDir(path),
|
|
121
|
+
fileIcon: this.fileIcon
|
|
122
|
+
})
|
|
123
|
+
this.fileSelector.init()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
moveFile(file, requestedPath) {
|
|
127
|
+
return moveFile(file.id, file.title, requestedPath, this.moveUrl)
|
|
128
|
+
.then(path => {
|
|
129
|
+
addAlert(
|
|
130
|
+
"success",
|
|
131
|
+
`${this.successMessage}: '${shortFileTitle(file.title, path)}'`
|
|
132
|
+
)
|
|
133
|
+
this.succcessCallback(file, path)
|
|
134
|
+
})
|
|
135
|
+
.catch(() => {
|
|
136
|
+
addAlert(
|
|
137
|
+
"error",
|
|
138
|
+
`${this.errorMessage}: '${shortFileTitle(file.title, file.path)}'`
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|