@fiduswriter/image-manager 0.1.0

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.
Files changed (63) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +5 -0
  3. package/dist/copyright_dialog/index.d.ts +17 -0
  4. package/dist/copyright_dialog/index.d.ts.map +1 -0
  5. package/dist/copyright_dialog/index.js +148 -0
  6. package/dist/copyright_dialog/index.js.map +1 -0
  7. package/dist/copyright_dialog/templates.d.ts +13 -0
  8. package/dist/copyright_dialog/templates.d.ts.map +1 -0
  9. package/dist/copyright_dialog/templates.js +48 -0
  10. package/dist/copyright_dialog/templates.js.map +1 -0
  11. package/dist/database.d.ts +9 -0
  12. package/dist/database.d.ts.map +1 -0
  13. package/dist/database.js +51 -0
  14. package/dist/database.js.map +1 -0
  15. package/dist/edit_dialog/index.d.ts +24 -0
  16. package/dist/edit_dialog/index.d.ts.map +1 -0
  17. package/dist/edit_dialog/index.js +186 -0
  18. package/dist/edit_dialog/index.js.map +1 -0
  19. package/dist/edit_dialog/model.d.ts +20 -0
  20. package/dist/edit_dialog/model.d.ts.map +1 -0
  21. package/dist/edit_dialog/model.js +153 -0
  22. package/dist/edit_dialog/model.js.map +1 -0
  23. package/dist/edit_dialog/templates.d.ts +5 -0
  24. package/dist/edit_dialog/templates.d.ts.map +1 -0
  25. package/dist/edit_dialog/templates.js +33 -0
  26. package/dist/edit_dialog/templates.js.map +1 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +5 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/overview/categories.d.ts +8 -0
  32. package/dist/overview/categories.d.ts.map +1 -0
  33. package/dist/overview/categories.js +94 -0
  34. package/dist/overview/categories.js.map +1 -0
  35. package/dist/overview/index.d.ts +39 -0
  36. package/dist/overview/index.d.ts.map +1 -0
  37. package/dist/overview/index.js +358 -0
  38. package/dist/overview/index.js.map +1 -0
  39. package/dist/overview/menu.d.ts +48 -0
  40. package/dist/overview/menu.d.ts.map +1 -0
  41. package/dist/overview/menu.js +67 -0
  42. package/dist/overview/menu.js.map +1 -0
  43. package/dist/overview/templates.d.ts +4 -0
  44. package/dist/overview/templates.d.ts.map +1 -0
  45. package/dist/overview/templates.js +24 -0
  46. package/dist/overview/templates.js.map +1 -0
  47. package/dist/selection_dialog/index.d.ts +19 -0
  48. package/dist/selection_dialog/index.d.ts.map +1 -0
  49. package/dist/selection_dialog/index.js +149 -0
  50. package/dist/selection_dialog/index.js.map +1 -0
  51. package/package.json +53 -0
  52. package/src/copyright_dialog/index.js +179 -0
  53. package/src/copyright_dialog/templates.js +56 -0
  54. package/src/database.js +57 -0
  55. package/src/edit_dialog/index.js +222 -0
  56. package/src/edit_dialog/model.js +184 -0
  57. package/src/edit_dialog/templates.js +41 -0
  58. package/src/index.js +4 -0
  59. package/src/overview/categories.js +115 -0
  60. package/src/overview/index.js +435 -0
  61. package/src/overview/menu.js +75 -0
  62. package/src/overview/templates.js +30 -0
  63. package/src/selection_dialog/index.js +176 -0
@@ -0,0 +1,435 @@
1
+ import {baseBodyTemplate} from "@fiduswriter/common/common"
2
+ import {FeedbackTab} from "@fiduswriter/common/feedback"
3
+ import {SiteMenu} from "@fiduswriter/common/menu"
4
+ import {
5
+ Dialog,
6
+ OverviewDataTable,
7
+ OverviewMenuView,
8
+ activateWait,
9
+ addAlert,
10
+ deactivateWait,
11
+ ensureCSS,
12
+ escapeText,
13
+ findTarget,
14
+ gettext,
15
+ isActivationEvent,
16
+ localizeDate,
17
+ post,
18
+ setDocTitle,
19
+ staticUrl,
20
+ whenReady
21
+ } from "fwtoolkit"
22
+ import {ImageOverviewCategories} from "./categories"
23
+ import {bulkMenuModel, menuModel} from "./menu"
24
+ /** Helper functions for user added images/SVGs.*/
25
+
26
+ export class ImageOverview {
27
+ constructor({app, user, plugins = []}) {
28
+ this.app = app
29
+ this.user = user
30
+ this.plugins = plugins
31
+ this.mod = {}
32
+
33
+ this.lastSort = {column: 0, dir: "asc"}
34
+ }
35
+
36
+ init() {
37
+ ensureCSS([
38
+ staticUrl("css/dialog_usermedia.css"),
39
+ staticUrl("css/dot_menu.css")
40
+ ])
41
+
42
+ return whenReady().then(() => {
43
+ this.render()
44
+ new ImageOverviewCategories(this)
45
+ const smenu = new SiteMenu(this.app, "images")
46
+ smenu.init()
47
+ this.menu = new OverviewMenuView(this, menuModel)
48
+ this.menu.init()
49
+ this.activatePlugins()
50
+ this.bindEvents()
51
+ this.mod.categories.setImageCategoryList(this.app.imageDB.cats)
52
+ this.initTable(Object.keys(this.app.imageDB.db))
53
+ // Reset scroll position to top to prevent Safari from auto-scrolling
54
+ // to the focused table element, which would hide the header/menu
55
+ window.scrollTo(0, 0)
56
+ })
57
+ }
58
+
59
+ render() {
60
+ this.dom = document.createElement("body")
61
+ this.dom.innerHTML = baseBodyTemplate({
62
+ contents: "",
63
+ user: this.user,
64
+ hasOverview: true,
65
+ app: this.app
66
+ })
67
+ document.body = this.dom
68
+ ensureCSS([staticUrl("css/cropper.min.css")])
69
+ setDocTitle(gettext("Media Manager"), this.app)
70
+ const feedbackTab = new FeedbackTab()
71
+ feedbackTab.init()
72
+ }
73
+
74
+ activatePlugins() {
75
+ if (this.pluginsActivated) {
76
+ // Plugins have been activated already
77
+ return
78
+ }
79
+ this.pluginsActivated = true
80
+ // Add plugins.
81
+ this.plugins = {}
82
+
83
+ return Promise.all(
84
+ this.plugins.map(([app, plugin]) => {
85
+ if (!this.app.settings.APPS.includes(app)) {
86
+ return Promise.resolve()
87
+ }
88
+ return Promise.all(
89
+ Object.values(plugin).map(pluginExport => {
90
+ if (typeof pluginExport === "function") {
91
+ this.plugins[pluginExport.name] = new pluginExport(
92
+ this
93
+ )
94
+ return (
95
+ this.plugins[pluginExport.name].init() ||
96
+ Promise.resolve()
97
+ )
98
+ }
99
+ return Promise.resolve()
100
+ })
101
+ )
102
+ })
103
+ )
104
+ }
105
+
106
+ //delete image
107
+ deleteImage(ids) {
108
+ ids = ids.map(id => Number.parseInt(id))
109
+ if (this.app.isOffline()) {
110
+ addAlert(
111
+ "error",
112
+ gettext(
113
+ "You are currently offline. Please try again when you are back online."
114
+ )
115
+ )
116
+ return
117
+ }
118
+ activateWait()
119
+ post("/api/usermedia/delete/", {ids})
120
+ .catch(error => {
121
+ addAlert("error", gettext("The image(s) could not be deleted"))
122
+ deactivateWait()
123
+ if (this.app.isOffline()) {
124
+ addAlert(
125
+ "error",
126
+ gettext(
127
+ "You are currently offline. Please try again when you are back online."
128
+ )
129
+ )
130
+ } else {
131
+ throw error
132
+ }
133
+ })
134
+ .then(() => {
135
+ ids.forEach(id => delete this.app.imageDB.db[id])
136
+ this.removeTableRows(ids)
137
+ addAlert("success", gettext("The image(s) have been deleted"))
138
+ })
139
+ .then(() => deactivateWait())
140
+ }
141
+
142
+ deleteImageDialog(ids) {
143
+ const buttons = [
144
+ {
145
+ text: gettext("Delete"),
146
+ classes: "fw-dark",
147
+ click: () => {
148
+ this.deleteImage(ids)
149
+ dialog.close()
150
+ }
151
+ },
152
+ {
153
+ type: "cancel"
154
+ }
155
+ ]
156
+ const dialog = new Dialog({
157
+ id: "confirmdeletion",
158
+ icon: "exclamation-triangle",
159
+ title: gettext("Confirm deletion"),
160
+ body: `<p>${gettext("Delete the image(s)")}?</p>`,
161
+ buttons
162
+ })
163
+ dialog.open()
164
+ }
165
+
166
+ updateTable(ids) {
167
+ // Remove items that already exist
168
+ this.removeTableRows(ids)
169
+ this.table.insert({data: ids.map(id => this.createTableRow(id))})
170
+ // Redo last sort
171
+ this.table.columns.sort(this.lastSort.column, this.lastSort.dir)
172
+ }
173
+
174
+ createTableRow(id) {
175
+ const image = this.app.imageDB.db[id]
176
+ const cats = image.cats.map(cat => `cat_${cat}`)
177
+
178
+ let fileType = image.file_type.split("/")
179
+
180
+ if (1 < fileType.length) {
181
+ fileType = fileType[1].toUpperCase()
182
+ } else {
183
+ fileType = fileType[0].toUpperCase()
184
+ }
185
+
186
+ return [
187
+ id,
188
+ false, // checkbox
189
+ `<span class="fw-usermedia-image ${cats.join(" ")}">
190
+ <img src="${image.thumbnail ? image.thumbnail : image.image}">
191
+ </span>
192
+ <span class="fw-usermedia-title">
193
+ <span class="edit-image fw-link-text fw-searchable" data-id="${id}">
194
+ ${image.title.length ? escapeText(image.title) : gettext("Untitled")}
195
+ </span>
196
+ <span class="fw-usermedia-type">${fileType}</span>
197
+ </span>`,
198
+ `<span>${image.width} x ${image.height}</span>`,
199
+ `<span class="fw-date">${localizeDate(image.added, "sortable-date")}</span>`,
200
+ `<span class="delete-image fw-link-text" data-id="${id}">
201
+ <i class="fa fa-trash-alt"></i>
202
+ </span>`
203
+ ]
204
+ }
205
+
206
+ removeTableRows(ids) {
207
+ ids = ids.map(id => Number.parseInt(id))
208
+
209
+ const existingRows = this.table.data.data
210
+ .map((row, index) => {
211
+ const id = row.cells[0].data
212
+ if (ids.includes(id)) {
213
+ return index
214
+ } else {
215
+ return false
216
+ }
217
+ })
218
+ .filter(rowIndex => rowIndex !== false)
219
+
220
+ if (existingRows.length) {
221
+ this.table.rows.remove(existingRows)
222
+ }
223
+ }
224
+
225
+ onResize() {
226
+ if (!this.table) {
227
+ return
228
+ }
229
+ this.initTable(Object.keys(this.app.imageDB.db))
230
+ }
231
+
232
+ /* Initialize the overview table */
233
+ initTable(ids) {
234
+ if (this.overviewTable) {
235
+ this.overviewTable.destroy()
236
+ this.overviewTable = null
237
+ }
238
+ this.table = null
239
+ this.dtBulk = null
240
+
241
+ const contentsEl = this.dom.querySelector(".fw-contents")
242
+ contentsEl.innerHTML = ""
243
+
244
+ const hiddenCols = [0]
245
+
246
+ if (window.innerWidth < 500) {
247
+ hiddenCols.push(1)
248
+ }
249
+
250
+ this.overviewTable = new OverviewDataTable({
251
+ dom: contentsEl,
252
+ classes: ["fw-data-table", "fw-large"],
253
+ columns: [
254
+ {
255
+ select: 0,
256
+ type: "number"
257
+ },
258
+ {
259
+ select: 1,
260
+ type: "boolean"
261
+ },
262
+ {
263
+ select: hiddenCols,
264
+ hidden: true
265
+ },
266
+ {
267
+ select: [1, 3, 5],
268
+ sortable: false
269
+ },
270
+ {
271
+ select: [this.lastSort.column],
272
+ sort: this.lastSort.dir
273
+ }
274
+ ],
275
+ data: ids.map(id => this.createTableRow(id)),
276
+ idColumn: 0,
277
+ checkboxColumn: 1,
278
+ bulkMenu: bulkMenuModel(),
279
+ bulkMenuPage: this,
280
+ searchable: true,
281
+ scrollY: `${Math.max(window.innerHeight - 360, 100)}px`,
282
+ tabIndex: 1,
283
+ labels: {
284
+ noRows: gettext("No images available"), // Message shown when there are no images
285
+ noResults: gettext("No images found") // Message shown when no images are found after search
286
+ },
287
+ headings: [
288
+ "",
289
+ "",
290
+ gettext("File"),
291
+ gettext("Size (px)"),
292
+ gettext("Added"),
293
+ ""
294
+ ],
295
+ template: (options, _dom) =>
296
+ `<div class='${options.classes.container}'${options.scrollY.length ? ` style='height: ${options.scrollY}; overflow-Y: auto;'` : ""}></div>
297
+ <div class='${options.classes.bottom}'>
298
+ ${
299
+ options.paging
300
+ ? `<div class='${options.classes.info}'></div>`
301
+ : ""
302
+ }
303
+ <nav class='${options.classes.pagination}'></nav>
304
+ </div>`,
305
+ rowRender: (row, tr, _index) => {
306
+ const id = row.cells[0].data
307
+ const inputNode = {
308
+ nodeName: "input",
309
+ attributes: {
310
+ type: "checkbox",
311
+ class: "entry-select fw-check",
312
+ "data-id": id,
313
+ id: `doc-img-${id}`
314
+ }
315
+ }
316
+ if (row.cells[1].data) {
317
+ inputNode.attributes.checked = true
318
+ }
319
+ tr.childNodes[0].childNodes = [
320
+ inputNode,
321
+ {
322
+ nodeName: "label",
323
+ attributes: {
324
+ for: `doc-img-${id}`
325
+ }
326
+ }
327
+ ]
328
+ },
329
+ onEnter: (row, _event) => {
330
+ if (this.getSelected().length > 0) {
331
+ return
332
+ }
333
+ const rowIndex = this.table.data.data.indexOf(row)
334
+ const button = this.table.dom.querySelector(
335
+ `tr[data-index="${rowIndex}"] span.edit-image`
336
+ )
337
+ if (button) {
338
+ button.click()
339
+ }
340
+ },
341
+ onDelete: row => {
342
+ const imageId = row.cells[0].data
343
+ this.deleteImageDialog([imageId])
344
+ }
345
+ })
346
+ this.overviewTable.init()
347
+ this.table = this.overviewTable.table
348
+ this.table.id = "imagelist"
349
+ this.dtBulk = this.overviewTable.dtBulk
350
+
351
+ this.table.on("datatable.sort", (column, dir) => {
352
+ this.lastSort = {column, dir}
353
+ })
354
+
355
+ this.table.dom.focus()
356
+ }
357
+
358
+ // get IDs of selected bib entries
359
+ getSelected() {
360
+ return Array.from(
361
+ this.dom.querySelectorAll(".entry-select:checked:not(:disabled)")
362
+ ).map(el => Number.parseInt(el.getAttribute("data-id")))
363
+ }
364
+
365
+ bindEvents() {
366
+ this.dom.addEventListener("click", event =>
367
+ this.handleActivation(event)
368
+ )
369
+ this.dom.addEventListener("keydown", event =>
370
+ this.handleActivation(event)
371
+ )
372
+ }
373
+
374
+ handleActivation(event) {
375
+ if (!isActivationEvent(event)) {
376
+ return
377
+ }
378
+ const el = {}
379
+ switch (true) {
380
+ case findTarget(event, ".delete-image", el): {
381
+ const imageId = el.target.dataset.id
382
+ this.deleteImageDialog([imageId])
383
+ break
384
+ }
385
+ case findTarget(event, ".edit-image", el): {
386
+ const imageId = el.target.dataset.id
387
+ import("../edit_dialog").then(({ImageEditDialog}) => {
388
+ const dialog = new ImageEditDialog(
389
+ this.app.imageDB,
390
+ imageId,
391
+ this
392
+ )
393
+ dialog.init().then(() => {
394
+ this.updateTable([imageId])
395
+ })
396
+ })
397
+ break
398
+ }
399
+ case findTarget(event, ".fw-add-input", el): {
400
+ const itemEl = el.target.closest(".fw-list-input")
401
+ if (!itemEl.nextElementSibling) {
402
+ itemEl.insertAdjacentHTML(
403
+ "afterend",
404
+ `<tr class="fw-list-input">
405
+ <td>
406
+ <input type="text" class="category-form">
407
+ <span class="fw-add-input icon-addremove" tabindex="0"></span>
408
+ </td>
409
+ </tr>`
410
+ )
411
+ } else {
412
+ itemEl.parentElement.removeChild(itemEl)
413
+ }
414
+ break
415
+ }
416
+ default:
417
+ break
418
+ }
419
+ }
420
+
421
+ close() {
422
+ if (this.table) {
423
+ this.table.destroy()
424
+ this.table = null
425
+ }
426
+ if (this.dtBulk) {
427
+ this.dtBulk.destroy()
428
+ this.dtBulk = null
429
+ }
430
+ if (this.menu) {
431
+ this.menu.destroy()
432
+ this.menu = null
433
+ }
434
+ }
435
+ }
@@ -0,0 +1,75 @@
1
+ import {gettext} from "fwtoolkit"
2
+
3
+ export const bulkMenuModel = () => ({
4
+ content: [
5
+ {
6
+ title: gettext("Delete selected"),
7
+ tooltip: gettext("Delete selected images."),
8
+ action: overview => {
9
+ const ids = overview.getSelected()
10
+ if (ids.length) {
11
+ overview.deleteImageDialog(ids)
12
+ }
13
+ },
14
+ disabled: overview =>
15
+ !overview.getSelected().length || overview.app.isOffline()
16
+ }
17
+ ]
18
+ })
19
+
20
+ export const menuModel = () => ({
21
+ content: [
22
+ {
23
+ type: "dropdown",
24
+ id: "cat_selector",
25
+ keys: "Alt-c",
26
+ content: [
27
+ {
28
+ title: gettext("All categories"),
29
+ action: _overview => {
30
+ const trs = document.querySelectorAll(
31
+ "#imagelist > tbody > tr"
32
+ )
33
+ trs.forEach(tr => (tr.style.display = ""))
34
+ }
35
+ }
36
+ ],
37
+ order: 1
38
+ },
39
+ {
40
+ type: "text",
41
+ title: gettext("Edit categories"),
42
+ keys: "Alt-e",
43
+ action: overview => overview.mod.categories.editCategoryDialog(),
44
+ order: 2,
45
+ disabled: overview => overview.app.isOffline()
46
+ },
47
+ {
48
+ type: "text",
49
+ title: gettext("Upload new image"),
50
+ keys: "Alt-u",
51
+ action: overview => {
52
+ import("../edit_dialog").then(({ImageEditDialog}) => {
53
+ const imageUpload = new ImageEditDialog(
54
+ overview.app.imageDB,
55
+ false,
56
+ overview
57
+ )
58
+ imageUpload.init().then(imageId => {
59
+ overview.updateTable([imageId])
60
+ })
61
+ })
62
+ },
63
+ order: 3,
64
+ disabled: overview => overview.app.isOffline()
65
+ },
66
+ {
67
+ type: "search",
68
+ icon: "search",
69
+ title: gettext("Search images"),
70
+ keys: "Alt-s",
71
+ input: (overview, text) => overview.table.search(text),
72
+ order: 4
73
+ }
74
+ ]
75
+ })
@@ -0,0 +1,30 @@
1
+ import {escapeText} from "fwtoolkit"
2
+
3
+ /** A template for the image category edit form. */
4
+ const usermediaCategoryformsTemplate = ({categories}) =>
5
+ `${categories
6
+ .map(
7
+ cat =>
8
+ `<tr id="categoryTr_${cat.id}" class="fw-list-input">
9
+ <td>
10
+ <input type="text" class="category-form" id="categoryTitle_${cat.id}"
11
+ value="${escapeText(cat.category_title)}" data-id="${cat.id}" />
12
+ <span class="fw-add-input icon-addremove" tabindex="0"></span>
13
+ </td>
14
+ </tr>`
15
+ )
16
+ .join("")}
17
+ <tr class="fw-list-input">
18
+ <td>
19
+ <input type="text" class="category-form" />
20
+ <span class="fw-add-input icon-addremove" tabindex="0"></span>
21
+ </td>
22
+ </tr>`
23
+
24
+ /** A template to edit image categories. */
25
+ export const usermediaEditcategoriesTemplate = ({categories}) =>
26
+ `<table id="editCategoryList" class="fw-dialog-table">
27
+ <tbody>
28
+ ${usermediaCategoryformsTemplate({categories})}
29
+ </tbody>
30
+ </table>`