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,37 @@
|
|
|
1
|
+
import {Dialog} from "../dialog.js"
|
|
2
|
+
import {newFolderTemplate} from "./templates.js"
|
|
3
|
+
|
|
4
|
+
export class NewFolderDialog {
|
|
5
|
+
constructor(callback = _foldername => {}) {
|
|
6
|
+
this.callback = callback
|
|
7
|
+
this.dialog = new Dialog({
|
|
8
|
+
title: gettext("New folder"),
|
|
9
|
+
id: "new-folder",
|
|
10
|
+
width: 400,
|
|
11
|
+
height: 150,
|
|
12
|
+
body: newFolderTemplate(),
|
|
13
|
+
buttons: [
|
|
14
|
+
{type: "cancel"},
|
|
15
|
+
{
|
|
16
|
+
text: gettext("Create folder"),
|
|
17
|
+
classes: "fw-dark",
|
|
18
|
+
click: () => {
|
|
19
|
+
const folderName =
|
|
20
|
+
this.dialog.dialogEl.querySelector(
|
|
21
|
+
"#new-folder-name"
|
|
22
|
+
).value
|
|
23
|
+
this.dialog.close()
|
|
24
|
+
if (!folderName.length) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
this.callback(folderName)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
open() {
|
|
35
|
+
return this.dialog.open()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import {escapeText, findTarget} from "../basic.js"
|
|
2
|
+
import {ensureCSS} from "../network.js"
|
|
3
|
+
|
|
4
|
+
export class FileSelector {
|
|
5
|
+
constructor({
|
|
6
|
+
dom,
|
|
7
|
+
files,
|
|
8
|
+
showFiles = true,
|
|
9
|
+
selectFolders = true,
|
|
10
|
+
multiSelect = false,
|
|
11
|
+
selectDir = _path => {},
|
|
12
|
+
selectFile = _path => {},
|
|
13
|
+
fileIcon = "far fa-file-alt"
|
|
14
|
+
}) {
|
|
15
|
+
this.dom = dom
|
|
16
|
+
this.files = files
|
|
17
|
+
this.showFiles = showFiles // Whether to show existing files or only folders
|
|
18
|
+
this.selectFolders = selectFolders // Whether to allow the selection of folders
|
|
19
|
+
this.multiSelect = multiSelect // Whether to allow the selectioj of multiple entries
|
|
20
|
+
this.selectDir = selectDir
|
|
21
|
+
this.selectFile = selectFile
|
|
22
|
+
this.fileIcon = fileIcon // File icon to use
|
|
23
|
+
this.root = {
|
|
24
|
+
name: "/",
|
|
25
|
+
type: "folder",
|
|
26
|
+
open: true,
|
|
27
|
+
selected: false,
|
|
28
|
+
path: "/",
|
|
29
|
+
children: []
|
|
30
|
+
}
|
|
31
|
+
this.selected = []
|
|
32
|
+
if (this.selectFolders && !this.multiSelect) {
|
|
33
|
+
this.root.selected = true
|
|
34
|
+
this.selected.push(this.root)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init() {
|
|
39
|
+
this.readDirStructure()
|
|
40
|
+
this.sortDirStructure()
|
|
41
|
+
ensureCSS(staticUrl("css/file_selector.css"))
|
|
42
|
+
this.dom.classList.add("fw-file-selector")
|
|
43
|
+
this.render()
|
|
44
|
+
this.bind()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
readDirStructure() {
|
|
48
|
+
// Read directory structure from existing file paths.
|
|
49
|
+
// A file's title is used as the final path segment when the file has
|
|
50
|
+
// no explicit folder path. We strip any "/" from that segment because
|
|
51
|
+
// some titles (e.g. E2EE ciphertext encoded in standard base64) contain
|
|
52
|
+
// "/" characters which would otherwise be mis-interpreted as path
|
|
53
|
+
// separators, nesting the file inside phantom collapsed folders.
|
|
54
|
+
this.files.forEach(file => {
|
|
55
|
+
let treeWalker = this.root.children
|
|
56
|
+
let path = file.path
|
|
57
|
+
if (!path.length || path.endsWith("/")) {
|
|
58
|
+
const safeName = (file.title || gettext("Untitled")).replace(
|
|
59
|
+
/\//g,
|
|
60
|
+
"\u2215" // U+2215 DIVISION SLASH — visually identical, not a path separator
|
|
61
|
+
)
|
|
62
|
+
path += safeName
|
|
63
|
+
}
|
|
64
|
+
const pathParts = path.split("/")
|
|
65
|
+
pathParts.forEach((pathPart, pathIndex) => {
|
|
66
|
+
if (!pathPart.length) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (pathIndex === pathParts.length - 1) {
|
|
70
|
+
if (this.showFiles) {
|
|
71
|
+
treeWalker.push({
|
|
72
|
+
name: pathPart,
|
|
73
|
+
type: "file",
|
|
74
|
+
path: pathParts.slice(0, pathIndex + 1).join("/"),
|
|
75
|
+
file
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
let folder = treeWalker.find(
|
|
81
|
+
item => item.name === pathPart && item.type === "folder"
|
|
82
|
+
)
|
|
83
|
+
if (!folder) {
|
|
84
|
+
folder = {
|
|
85
|
+
name: pathPart,
|
|
86
|
+
type: "folder",
|
|
87
|
+
open: false,
|
|
88
|
+
selected: false,
|
|
89
|
+
path: pathParts.slice(0, pathIndex + 1).join("/") + "/",
|
|
90
|
+
children: []
|
|
91
|
+
}
|
|
92
|
+
treeWalker.push(folder)
|
|
93
|
+
}
|
|
94
|
+
treeWalker = folder.children
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sortDirStructure(entries = this.root.children) {
|
|
100
|
+
entries.sort((a, b) => {
|
|
101
|
+
if (a.type !== b.type) {
|
|
102
|
+
return a.type === "folder" ? -1 : 1
|
|
103
|
+
}
|
|
104
|
+
return a.name > b.name ? 1 : -1
|
|
105
|
+
})
|
|
106
|
+
entries.forEach(entry => {
|
|
107
|
+
if (entry.type === "folder" && entry.children.length) {
|
|
108
|
+
this.sortDirStructure(entry.children)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
addFolder(rawName) {
|
|
114
|
+
const name = rawName.replace(/\//g, "")
|
|
115
|
+
// Add a new folder as a subfolder to the currently selected folder
|
|
116
|
+
if (
|
|
117
|
+
!this.selected.length ||
|
|
118
|
+
this.selected[0].type !== "folder" ||
|
|
119
|
+
this.selected[0].children.find(
|
|
120
|
+
child => child.type === "folder" && child.name === name
|
|
121
|
+
)
|
|
122
|
+
) {
|
|
123
|
+
// A file is selected. Give up.
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
const newFolder = {
|
|
127
|
+
name,
|
|
128
|
+
type: "folder",
|
|
129
|
+
open: true,
|
|
130
|
+
selected: true,
|
|
131
|
+
path: this.selected[0].path + name + "/",
|
|
132
|
+
children: []
|
|
133
|
+
}
|
|
134
|
+
this.selected[0].children.push(newFolder)
|
|
135
|
+
this.sortDirStructure(this.selected[0].children)
|
|
136
|
+
this.selected[0].open = true
|
|
137
|
+
if (!this.multiSelect) {
|
|
138
|
+
this.selected[0].selected = false
|
|
139
|
+
this.selected = []
|
|
140
|
+
}
|
|
141
|
+
this.selected.push(newFolder)
|
|
142
|
+
this.selectDir(newFolder.path)
|
|
143
|
+
this.render()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
deselectAll() {
|
|
147
|
+
this.selected.forEach(entry => (entry.selected = false))
|
|
148
|
+
this.selected = []
|
|
149
|
+
this.render()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
render() {
|
|
153
|
+
this.dom.innerHTML = this.renderFolder(this.root)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
renderFolder(folder, indentLevel = 0) {
|
|
157
|
+
let returnString = ""
|
|
158
|
+
returnString += `<div class="folder${folder.open ? "" : " closed"}">`
|
|
159
|
+
returnString += `<p style="margin-left:${indentLevel * 10}px;">${
|
|
160
|
+
folder.children.length
|
|
161
|
+
? `<i class="far fa-${folder.open ? "minus" : "plus"}-square"></i> `
|
|
162
|
+
: ""
|
|
163
|
+
}<span class="folder-name${folder.selected ? " selected" : ""}"><i class="fas fa-folder"></i> ${escapeText(folder.name)}</span></p>`
|
|
164
|
+
if (folder.open) {
|
|
165
|
+
returnString += '<div class="folder-content">'
|
|
166
|
+
returnString += folder.children
|
|
167
|
+
.map(child => {
|
|
168
|
+
if (child.type === "folder") {
|
|
169
|
+
return this.renderFolder(child, indentLevel + 1)
|
|
170
|
+
} else {
|
|
171
|
+
return `<p class="file" style="margin-left:${(indentLevel + 1) * 10 + 20}px;"><span class="file-name${child.selected ? " selected" : ""}"><i class="${this.fileIcon}"></i> ${escapeText(child.name)}</span></p>`
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
.join("")
|
|
175
|
+
returnString += "</div>"
|
|
176
|
+
}
|
|
177
|
+
returnString += "</div>"
|
|
178
|
+
return returnString
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
findEntry(dom) {
|
|
182
|
+
const searchPath = []
|
|
183
|
+
let seekItem = dom
|
|
184
|
+
while (seekItem.closest("div.folder, p.file")) {
|
|
185
|
+
let itemNumber = 0
|
|
186
|
+
seekItem = seekItem.closest("div.folder, p.file")
|
|
187
|
+
while (seekItem.previousElementSibling) {
|
|
188
|
+
itemNumber++
|
|
189
|
+
seekItem = seekItem.previousElementSibling
|
|
190
|
+
}
|
|
191
|
+
searchPath.push(itemNumber)
|
|
192
|
+
seekItem = seekItem.parentElement
|
|
193
|
+
}
|
|
194
|
+
let entry = this.root
|
|
195
|
+
searchPath.pop()
|
|
196
|
+
while (searchPath.length) {
|
|
197
|
+
entry = entry.children[searchPath.pop()]
|
|
198
|
+
}
|
|
199
|
+
return entry
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
bind() {
|
|
203
|
+
this.dom.addEventListener("click", event => {
|
|
204
|
+
const el = {}
|
|
205
|
+
switch (true) {
|
|
206
|
+
case findTarget(event, ".fa-plus-square", el): {
|
|
207
|
+
event.preventDefault()
|
|
208
|
+
const entry = this.findEntry(el.target)
|
|
209
|
+
entry.open = true
|
|
210
|
+
this.render()
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
case findTarget(event, ".fa-minus-square", el): {
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
const entry = this.findEntry(el.target)
|
|
216
|
+
entry.open = false
|
|
217
|
+
this.render()
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
case findTarget(event, ".folder-name", el): {
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
if (!this.selectFolders) {
|
|
223
|
+
// Folders cannot be selected
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
const entry = this.findEntry(el.target)
|
|
227
|
+
if (this.selected.includes(entry)) {
|
|
228
|
+
entry.selected = false
|
|
229
|
+
this.selected = this.selected.filter(e => e !== entry)
|
|
230
|
+
this.render()
|
|
231
|
+
} else {
|
|
232
|
+
entry.selected = true
|
|
233
|
+
if (!this.multiSelect && this.selected.length) {
|
|
234
|
+
this.selected[0].selected = false
|
|
235
|
+
}
|
|
236
|
+
this.selected.push(entry)
|
|
237
|
+
this.render()
|
|
238
|
+
this.selectDir(entry.path)
|
|
239
|
+
}
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
case findTarget(event, ".file-name", el): {
|
|
243
|
+
event.preventDefault()
|
|
244
|
+
const entry = this.findEntry(el.target)
|
|
245
|
+
if (this.selected.includes(entry)) {
|
|
246
|
+
entry.selected = false
|
|
247
|
+
this.selected = this.selected.filter(e => e !== entry)
|
|
248
|
+
this.render()
|
|
249
|
+
} else {
|
|
250
|
+
entry.selected = true
|
|
251
|
+
if (!this.multiSelect && this.selected.length) {
|
|
252
|
+
this.selected[0].selected = false
|
|
253
|
+
}
|
|
254
|
+
this.selected.push(entry)
|
|
255
|
+
this.render()
|
|
256
|
+
this.selectFile(entry.path)
|
|
257
|
+
}
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {escapeText} from "../index.js"
|
|
2
|
+
|
|
3
|
+
export const moveTemplate = ({path}) =>
|
|
4
|
+
`<div>
|
|
5
|
+
<span>${gettext("Path")}:</span>
|
|
6
|
+
<input type="text" value="${escapeText(path)}" id="path" placeholder="${gettext("Insert path")}">
|
|
7
|
+
<div class="file-selector"></div>
|
|
8
|
+
</div>`
|
|
9
|
+
|
|
10
|
+
export const newFolderTemplate = () =>
|
|
11
|
+
`<div><input type="text" id="new-folder-name" placeholder="${gettext("Insert folder name")}"></div>`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {escapeText} from "../basic.js"
|
|
2
|
+
import {postJson} from "../network.js"
|
|
3
|
+
|
|
4
|
+
export const shortFileTitle = (title, path) => {
|
|
5
|
+
if (!path.length || path.endsWith("/")) {
|
|
6
|
+
return escapeText(title || gettext("Untitled"))
|
|
7
|
+
}
|
|
8
|
+
return escapeText(path.split("/").pop())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const longFilePath = (title, path, prefix = "") => {
|
|
12
|
+
if (!path.length) {
|
|
13
|
+
path = "/"
|
|
14
|
+
}
|
|
15
|
+
if (path.endsWith("/")) {
|
|
16
|
+
path += title.replace(/\//g, "") || gettext("Untitled")
|
|
17
|
+
}
|
|
18
|
+
if (prefix.length) {
|
|
19
|
+
const pathParts = path.split("/")
|
|
20
|
+
const fileName = pathParts.pop()
|
|
21
|
+
pathParts.push(prefix + fileName)
|
|
22
|
+
path = pathParts.join("/")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return path
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const cleanPath = (title, path) => {
|
|
29
|
+
if (!path.startsWith("/")) {
|
|
30
|
+
path = "/" + path
|
|
31
|
+
}
|
|
32
|
+
path = path.replace(/\/{2,}/g, "/") // replace multiple backslashes
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
path.endsWith(
|
|
36
|
+
`/${title.replace(/\//g, "")}` || `/${gettext("Untitled")}`
|
|
37
|
+
)
|
|
38
|
+
) {
|
|
39
|
+
path = path.split("/").slice(0, -1).join("/") + "/"
|
|
40
|
+
}
|
|
41
|
+
if (path === "/") {
|
|
42
|
+
path = ""
|
|
43
|
+
}
|
|
44
|
+
return path
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const moveFile = (fileId, title, path, moveUrl) => {
|
|
48
|
+
path = cleanPath(title, path)
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
postJson(moveUrl, {id: fileId, path}).then(({json}) => {
|
|
51
|
+
if (json.done) {
|
|
52
|
+
resolve(path)
|
|
53
|
+
} else {
|
|
54
|
+
reject()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
}
|
package/src/focus.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Get the index number of currently focused selement. This it to set tyhe focus close by after doing some dom changes.
|
|
2
|
+
|
|
3
|
+
export const getFocusIndex = () => {
|
|
4
|
+
return Array.from(
|
|
5
|
+
document.querySelectorAll(
|
|
6
|
+
"a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex], [contenteditable]"
|
|
7
|
+
)
|
|
8
|
+
).findIndex(el => el === document.activeElement)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const setFocusIndex = index => {
|
|
12
|
+
const focusableElements = Array.from(
|
|
13
|
+
document.querySelectorAll(
|
|
14
|
+
"a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex], [contenteditable]"
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
if (index >= 0 && index < focusableElements.length) {
|
|
18
|
+
focusableElements[index].focus()
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,61 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {OverviewMenuView} from "./overview_menu.js"
|
|
2
|
+
export {
|
|
3
|
+
dropdownSelect,
|
|
4
|
+
setCheckableLabel,
|
|
5
|
+
activateWait,
|
|
6
|
+
deactivateWait,
|
|
7
|
+
addAlert,
|
|
8
|
+
langName,
|
|
9
|
+
localizeDate,
|
|
10
|
+
enableDatePicker,
|
|
11
|
+
noSpaceTmp,
|
|
12
|
+
escapeText,
|
|
13
|
+
unescapeText,
|
|
14
|
+
infoTooltip,
|
|
15
|
+
cancelPromise,
|
|
16
|
+
findTarget,
|
|
17
|
+
whenReady,
|
|
18
|
+
setDocTitle,
|
|
19
|
+
showSystemMessage
|
|
20
|
+
} from "./basic.js"
|
|
21
|
+
|
|
2
22
|
export {convertDataURIToBlob} from "./blob.js"
|
|
3
|
-
export {shortFileTitle, longFilePath} from "./file.js"
|
|
4
|
-
export {get, post, postJson} from "./network.js"
|
|
5
23
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
}
|
|
24
|
+
export {isActivationEvent} from "./events.js"
|
|
25
|
+
|
|
26
|
+
export {getFocusIndex, setFocusIndex} from "./focus.js"
|
|
11
27
|
|
|
12
|
-
export
|
|
28
|
+
export {
|
|
29
|
+
get,
|
|
30
|
+
getJson,
|
|
31
|
+
post,
|
|
32
|
+
postJson,
|
|
33
|
+
postBare,
|
|
34
|
+
ensureCSS,
|
|
35
|
+
getCookie
|
|
36
|
+
} from "./network.js"
|
|
37
|
+
export {
|
|
38
|
+
setLanguage,
|
|
39
|
+
avatarTemplate
|
|
40
|
+
} from "./user.js"
|
|
41
|
+
export {Dialog} from "./dialog.js"
|
|
42
|
+
export {ContentMenu} from "./content_menu.js"
|
|
43
|
+
export {makeWorker} from "./worker.js"
|
|
44
|
+
export {baseBodyTemplate} from "./templates.js"
|
|
45
|
+
export {WebSocketConnector} from "./ws.js"
|
|
46
|
+
export {filterPrimaryEmail} from "./user_util.js"
|
|
47
|
+
export {DatatableBulk} from "./datatable_bulk.js"
|
|
48
|
+
export {faqDialog} from "./faq_dialog.js"
|
|
49
|
+
export {
|
|
50
|
+
FileDialog,
|
|
51
|
+
FileSelector,
|
|
52
|
+
cleanPath,
|
|
53
|
+
moveFile,
|
|
54
|
+
shortFileTitle,
|
|
55
|
+
longFilePath,
|
|
56
|
+
NewFolderDialog
|
|
57
|
+
} from "./file/index.js"
|
|
58
|
+
export {
|
|
59
|
+
initSettings,
|
|
60
|
+
getSettings
|
|
61
|
+
} from "./settings.js"
|
package/src/network.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {getSettings} from "./settings.js"
|
|
2
|
+
|
|
3
|
+
/** Get cookie to set as part of the request header of all AJAX requests to the server.
|
|
4
|
+
* @param name The name of the token to look for in the cookie.
|
|
5
|
+
*/
|
|
6
|
+
export const getCookie = name => {
|
|
7
|
+
if (!document.cookie || document.cookie === "") {
|
|
8
|
+
return null
|
|
4
9
|
}
|
|
5
10
|
const cookie = document.cookie
|
|
6
11
|
.split(";")
|
|
7
12
|
.map(cookie => cookie.trim())
|
|
8
|
-
.find(cookie =>
|
|
9
|
-
|
|
13
|
+
.find(cookie => {
|
|
14
|
+
if (cookie.substring(0, name.length + 1) == name + "=") {
|
|
15
|
+
return true
|
|
16
|
+
} else {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
if (cookie) {
|
|
21
|
+
return decodeURIComponent(cookie.substring(name.length + 1))
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
10
24
|
}
|
|
11
25
|
|
|
26
|
+
/* from https://www.tjvantoll.com/2015/09/13/fetch-and-errors/ */
|
|
12
27
|
const handleFetchErrors = response => {
|
|
13
28
|
if (!response.ok) {
|
|
14
29
|
throw response
|
|
@@ -16,8 +31,11 @@ const handleFetchErrors = response => {
|
|
|
16
31
|
return response
|
|
17
32
|
}
|
|
18
33
|
|
|
19
|
-
export const get = (url, params = {}) => {
|
|
20
|
-
const
|
|
34
|
+
export const get = (url, params = {}, csrfToken = false) => {
|
|
35
|
+
const settings = getSettings()
|
|
36
|
+
if (!csrfToken) {
|
|
37
|
+
csrfToken = settings.getCsrfToken() // Won't work in web worker.
|
|
38
|
+
}
|
|
21
39
|
const queryString = Object.keys(params)
|
|
22
40
|
.map(
|
|
23
41
|
key =>
|
|
@@ -27,7 +45,7 @@ export const get = (url, params = {}) => {
|
|
|
27
45
|
if (queryString.length) {
|
|
28
46
|
url = `${url}?${queryString}`
|
|
29
47
|
}
|
|
30
|
-
return fetch(url, {
|
|
48
|
+
return fetch(settings.apiUrl(url), {
|
|
31
49
|
method: "GET",
|
|
32
50
|
headers: {
|
|
33
51
|
"X-CSRFToken": csrfToken,
|
|
@@ -38,8 +56,15 @@ export const get = (url, params = {}) => {
|
|
|
38
56
|
}).then(handleFetchErrors)
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
export const
|
|
42
|
-
|
|
59
|
+
export const getJson = (url, params = {}, csrfToken = false) =>
|
|
60
|
+
get(url, params, csrfToken).then(response => response.json())
|
|
61
|
+
|
|
62
|
+
export const postBare = (url, object = {}, files = {}, options = {}) => {
|
|
63
|
+
const settings = getSettings()
|
|
64
|
+
|
|
65
|
+
const {csrfToken: csrfTokenOpt, keepalive = false} = options
|
|
66
|
+
const csrfToken = csrfTokenOpt || settings.getCsrfToken() // Won't work in web worker.
|
|
67
|
+
|
|
43
68
|
const fetchOptions = {
|
|
44
69
|
method: "POST",
|
|
45
70
|
headers: {
|
|
@@ -47,7 +72,8 @@ export const post = (url, object = {}, files = {}) => {
|
|
|
47
72
|
Accept: "application/json",
|
|
48
73
|
"X-Requested-With": "XMLHttpRequest"
|
|
49
74
|
},
|
|
50
|
-
credentials: "include"
|
|
75
|
+
credentials: "include",
|
|
76
|
+
keepalive
|
|
51
77
|
}
|
|
52
78
|
|
|
53
79
|
if (Object.keys(files).length) {
|
|
@@ -70,10 +96,33 @@ export const post = (url, object = {}, files = {}) => {
|
|
|
70
96
|
fetchOptions.body = JSON.stringify(object)
|
|
71
97
|
}
|
|
72
98
|
|
|
73
|
-
return fetch(url, fetchOptions)
|
|
99
|
+
return fetch(settings.apiUrl(url), fetchOptions)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const post = (url, object = {}, files = {}, options = {}) => {
|
|
103
|
+
return postBare(url, object, files, options).then(handleFetchErrors)
|
|
74
104
|
}
|
|
75
105
|
|
|
76
|
-
|
|
77
|
-
|
|
106
|
+
// post json object and return json and status
|
|
107
|
+
export const postJson = (url, object = {}, files = {}, options = {}) =>
|
|
108
|
+
post(url, object, files, options).then(response =>
|
|
78
109
|
response.json().then(json => ({json, status: response.status}))
|
|
79
110
|
)
|
|
111
|
+
|
|
112
|
+
export const ensureCSS = cssUrl => {
|
|
113
|
+
if (typeof cssUrl === "object") {
|
|
114
|
+
cssUrl.forEach(url => ensureCSS(url))
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const link = document.createElement("link")
|
|
118
|
+
link.rel = "stylesheet"
|
|
119
|
+
link.href = cssUrl
|
|
120
|
+
const styleSheet = Array.from(document.styleSheets).find(
|
|
121
|
+
styleSheet => styleSheet.href === link.href
|
|
122
|
+
)
|
|
123
|
+
if (!styleSheet) {
|
|
124
|
+
document.head.appendChild(link)
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
return false
|
|
128
|
+
}
|