@fiduswriter/image-manager 0.1.13 → 0.1.16

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 (48) hide show
  1. package/dist/copyright_dialog/index.d.ts.map +1 -1
  2. package/dist/copyright_dialog/index.js +19 -20
  3. package/dist/copyright_dialog/index.js.map +1 -1
  4. package/dist/copyright_dialog/templates.d.ts +2 -2
  5. package/dist/copyright_dialog/templates.d.ts.map +1 -1
  6. package/dist/copyright_dialog/templates.js +3 -3
  7. package/dist/copyright_dialog/templates.js.map +1 -1
  8. package/dist/database.d.ts.map +1 -1
  9. package/dist/database.js +7 -8
  10. package/dist/database.js.map +1 -1
  11. package/dist/edit_dialog/index.d.ts.map +1 -1
  12. package/dist/edit_dialog/index.js +25 -23
  13. package/dist/edit_dialog/index.js.map +1 -1
  14. package/dist/edit_dialog/model.d.ts.map +1 -1
  15. package/dist/edit_dialog/model.js +14 -18
  16. package/dist/edit_dialog/model.js.map +1 -1
  17. package/dist/edit_dialog/templates.d.ts.map +1 -1
  18. package/dist/edit_dialog/templates.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/overview/categories.d.ts.map +1 -1
  23. package/dist/overview/categories.js +17 -20
  24. package/dist/overview/categories.js.map +1 -1
  25. package/dist/overview/index.d.ts.map +1 -1
  26. package/dist/overview/index.js +3 -2
  27. package/dist/overview/index.js.map +1 -1
  28. package/dist/overview/menu.d.ts.map +1 -1
  29. package/dist/overview/menu.js +13 -15
  30. package/dist/overview/menu.js.map +1 -1
  31. package/dist/selection_dialog/index.d.ts.map +1 -1
  32. package/dist/selection_dialog/index.js +22 -26
  33. package/dist/selection_dialog/index.js.map +1 -1
  34. package/dist/types.d.ts +10 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +8 -4
  37. package/src/copyright_dialog/index.ts +177 -186
  38. package/src/copyright_dialog/templates.ts +26 -19
  39. package/src/database.ts +61 -68
  40. package/src/edit_dialog/index.ts +261 -251
  41. package/src/edit_dialog/model.ts +220 -220
  42. package/src/edit_dialog/templates.ts +22 -22
  43. package/src/index.ts +5 -4
  44. package/src/overview/categories.ts +106 -116
  45. package/src/overview/index.ts +2 -2
  46. package/src/overview/menu.ts +77 -77
  47. package/src/selection_dialog/index.ts +197 -207
  48. package/src/types.ts +79 -65
@@ -1,287 +1,297 @@
1
- import {CheckableList, ContentMenu, Dialog, addAlert, gettext} from "fwtoolkit"
2
- import type {CheckableListOptions} from "fwtoolkit"
3
- import type {ContentMenuInit} from "fwtoolkit/content_menu"
4
- import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
1
+ import {
2
+ CheckableList,
3
+ ContentMenu,
4
+ Dialog,
5
+ addAlert,
6
+ gettext,
7
+ } from "fwtoolkit";
8
+ import type { CheckableListOptions } from "fwtoolkit";
9
+ import type { ContentMenuInit } from "fwtoolkit/content_menu";
10
+ import { E2EEEncryptor } from "fwtoolkit/e2ee/encryptor";
5
11
 
6
- import {imageEditModel} from "./model.js"
7
- import {imageEditTemplate} from "./templates.js"
8
- import type {ImageDB} from "../database.js"
12
+ import { imageEditModel } from "./model.js";
13
+ import { imageEditTemplate } from "./templates.js";
14
+ import type { ImageDB } from "../database.js";
9
15
  import type {
10
- Copyright,
11
- Image,
12
- ImageCategory,
13
- ImageManagerPage,
14
- SaveImageRequest
15
- } from "../types.js"
16
+ Copyright,
17
+ Image,
18
+ ImageCategory,
19
+ ImageManagerPage,
20
+ SaveImageRequest,
21
+ } from "../types.js";
16
22
 
17
23
  export class ImageEditDialog {
18
- imageDB: ImageDB
24
+ imageDB: ImageDB;
19
25
 
20
- page: ImageManagerPage
26
+ page: ImageManagerPage;
21
27
 
22
- imageId: number | false
28
+ imageId: number | false;
23
29
 
24
- dialog: Dialog | false = false
30
+ dialog: Dialog | false = false;
25
31
 
26
- copyright: Copyright
32
+ copyright: Copyright;
27
33
 
28
- menu: ContentMenuInit
34
+ menu: ContentMenuInit;
29
35
 
30
- catsList: CheckableList | {value: (string | number)[]} = {value: []}
36
+ catsList: CheckableList | { value: (string | number)[] } = { value: [] };
31
37
 
32
- mediaPreviewerDiv?: HTMLElement
38
+ mediaPreviewerDiv?: HTMLElement;
33
39
 
34
- mediaPreviewer?: HTMLElement
40
+ mediaPreviewer?: HTMLElement;
35
41
 
36
- rotation = 0
42
+ rotation = 0;
37
43
 
38
- cropped = false
44
+ cropped = false;
39
45
 
40
- mediaInput?: File
46
+ mediaInput?: File;
41
47
 
42
- constructor(imageDB: ImageDB, imageId: number | false = false, page: ImageManagerPage) {
43
- this.imageDB = imageDB
44
- this.page = page
45
- this.imageId = imageId
46
- this.copyright = this.imageId
47
- ? this.imageDB.db[this.imageId].copyright
48
- : {
49
- holder: false,
50
- year: false,
51
- freeToRead: true,
52
- licenses: []
53
- }
54
- this.menu =
55
- (this.page.menu as {imageEditModel?: ContentMenuInit} | undefined)
56
- ?.imageEditModel || imageEditModel()
57
- }
58
-
59
- //open a dialog for uploading an image
60
- init(): Promise<number | void> {
61
- if (this.page.app.isOffline()) {
62
- this.showOffline()
63
- return Promise.resolve()
64
- }
65
- const returnPromise = new Promise<number | void>(resolve => {
66
- let dialog: Dialog
67
- dialog = new Dialog({
68
- title: this.imageId
69
- ? gettext("Update Image Information")
70
- : gettext("Upload Image"),
71
- id: "editimage",
72
- classes: "fw-media-uploader",
73
- body: imageEditTemplate({
74
- image: this.imageId ? this.imageDB.db[this.imageId] : false,
75
- cats: this.imageDB.cats
76
- }),
77
- buttons: [
78
- {
79
- text: this.imageId
80
- ? gettext("Update")
81
- : gettext("Upload"),
82
- click: () => resolve(this.saveImage()),
83
- classes: "fw-dark"
84
- },
85
- {
86
- type: "cancel",
87
- classes: "fw-orange",
88
- click: () => dialog.close()
89
- }
90
- ]
91
- })
92
- this.dialog = dialog
93
- dialog.open()
94
- })
95
-
96
- const image: Image | false = this.imageId
97
- ? this.imageDB.db[this.imageId]
98
- : false
99
- const catsEl = document.getElementById("image-edit-categories")
100
- if (catsEl) {
101
- const checkableOptions: CheckableListOptions = {
102
- dom: catsEl,
103
- options: this.imageDB.cats.map((cat: ImageCategory) => ({
104
- id: cat.id,
105
- label: cat.category_title
106
- })),
107
- initialValue: image ? image.cats : [],
108
- multiple: true
109
- }
110
- this.catsList = new CheckableList(checkableOptions)
111
- } else {
112
- this.catsList = {value: []}
113
- }
48
+ constructor(
49
+ imageDB: ImageDB,
50
+ imageId: number | false = false,
51
+ page: ImageManagerPage,
52
+ ) {
53
+ this.imageDB = imageDB;
54
+ this.page = page;
55
+ this.imageId = imageId;
56
+ this.copyright = this.imageId
57
+ ? this.imageDB.db[this.imageId].copyright
58
+ : {
59
+ holder: false,
60
+ year: false,
61
+ freeToRead: true,
62
+ licenses: [],
63
+ };
64
+ this.menu =
65
+ (this.page.menu as { imageEditModel?: ContentMenuInit } | undefined)
66
+ ?.imageEditModel || imageEditModel();
67
+ }
114
68
 
115
- if (!this.imageId) {
116
- this.bindMediaUploadEvents()
117
- }
118
-
119
- const figureEditMenu = document.querySelector(".figure-edit-menu")
120
- if (figureEditMenu) {
121
- figureEditMenu.addEventListener("click", event => {
122
- event.preventDefault()
123
- event.stopImmediatePropagation()
69
+ //open a dialog for uploading an image
70
+ init(): Promise<number | void> {
71
+ if (this.page.app.isOffline()) {
72
+ this.showOffline();
73
+ return Promise.resolve();
74
+ }
75
+ const returnPromise = new Promise<number | void>((resolve) => {
76
+ let dialog: Dialog;
77
+ dialog = new Dialog({
78
+ title: this.imageId
79
+ ? gettext("Update Image Information")
80
+ : gettext("Upload Image"),
81
+ id: "editimage",
82
+ classes: "fw-media-uploader",
83
+ body: imageEditTemplate({
84
+ image: this.imageId ? this.imageDB.db[this.imageId] : false,
85
+ cats: this.imageDB.cats,
86
+ }),
87
+ buttons: [
88
+ {
89
+ text: this.imageId ? gettext("Update") : gettext("Upload"),
90
+ click: () => resolve(this.saveImage()),
91
+ classes: "fw-dark",
92
+ },
93
+ {
94
+ type: "cancel",
95
+ classes: "fw-orange",
96
+ click: () => dialog.close(),
97
+ },
98
+ ],
99
+ });
100
+ this.dialog = dialog;
101
+ dialog.open();
102
+ });
124
103
 
125
- const contentMenu = new ContentMenu({
126
- menu: this.menu,
127
- width: 220,
128
- page: this,
129
- menuPos: {X: (event as MouseEvent).pageX - 50, Y: (event as MouseEvent).pageY + 50}
130
- })
131
- contentMenu.open()
132
- })
133
- }
104
+ const image: Image | false = this.imageId
105
+ ? this.imageDB.db[this.imageId]
106
+ : false;
107
+ const catsEl = document.getElementById("image-edit-categories");
108
+ if (catsEl) {
109
+ const checkableOptions: CheckableListOptions = {
110
+ dom: catsEl,
111
+ options: this.imageDB.cats.map((cat: ImageCategory) => ({
112
+ id: cat.id,
113
+ label: cat.category_title,
114
+ })),
115
+ initialValue: image ? image.cats : [],
116
+ multiple: true,
117
+ };
118
+ this.catsList = new CheckableList(checkableOptions);
119
+ } else {
120
+ this.catsList = { value: [] };
121
+ }
134
122
 
135
- return returnPromise
123
+ if (!this.imageId) {
124
+ this.bindMediaUploadEvents();
136
125
  }
137
126
 
138
- //add image upload events
139
- bindMediaUploadEvents(): void {
140
- const selectButton = document.querySelector(
141
- "#editimage .fw-media-select-button"
142
- ) as HTMLButtonElement | null,
143
- mediaInputSelector = document.querySelector(
144
- "#editimage .fw-media-file-input"
145
- ) as HTMLInputElement | null
146
- this.mediaPreviewerDiv = document.querySelector(
147
- "#editimage .figure-preview > div"
148
- ) as HTMLElement | null || undefined
149
- this.rotation = 0
150
- this.cropped = false
127
+ const figureEditMenu = document.querySelector(".figure-edit-menu");
128
+ if (figureEditMenu) {
129
+ figureEditMenu.addEventListener("click", (event) => {
130
+ event.preventDefault();
131
+ event.stopImmediatePropagation();
151
132
 
152
- if (!selectButton || !mediaInputSelector || !this.mediaPreviewerDiv) {
153
- return
154
- }
133
+ const contentMenu = new ContentMenu({
134
+ menu: this.menu,
135
+ width: 220,
136
+ page: this,
137
+ menuPos: {
138
+ X: (event as MouseEvent).pageX - 50,
139
+ Y: (event as MouseEvent).pageY + 50,
140
+ },
141
+ });
142
+ contentMenu.open();
143
+ });
144
+ }
155
145
 
156
- selectButton.addEventListener("click", () => {
157
- mediaInputSelector.click()
158
- })
146
+ return returnPromise;
147
+ }
159
148
 
160
- const dialog = this.dialog
161
- if (!dialog) {
162
- return
163
- }
149
+ //add image upload events
150
+ bindMediaUploadEvents(): void {
151
+ const selectButton = document.querySelector(
152
+ "#editimage .fw-media-select-button",
153
+ ) as HTMLButtonElement | null,
154
+ mediaInputSelector = document.querySelector(
155
+ "#editimage .fw-media-file-input",
156
+ ) as HTMLInputElement | null;
157
+ this.mediaPreviewerDiv =
158
+ (document.querySelector(
159
+ "#editimage .figure-preview > div",
160
+ ) as HTMLElement | null) || undefined;
161
+ this.rotation = 0;
162
+ this.cropped = false;
164
163
 
165
- mediaInputSelector.addEventListener("change", () => {
166
- this.mediaInput = mediaInputSelector.files![0]
167
- const fr = new window.FileReader()
168
- fr.onload = () => {
169
- this.mediaPreviewerDiv!.innerHTML = `<div class="img" style="background-image: url(${fr.result});" />`
170
- this.mediaPreviewer =
171
- this.mediaPreviewerDiv!.querySelector(".img") || undefined
172
- this.mediaPreviewerDiv!.classList.remove("crop-mode")
173
- dialog.centerDialog()
174
- }
175
- fr.readAsDataURL(this.mediaInput)
176
- })
164
+ if (!selectButton || !mediaInputSelector || !this.mediaPreviewerDiv) {
165
+ return;
177
166
  }
178
167
 
179
- displayCreateImageError(errors: Record<string, string>): void {
180
- Object.keys(errors).forEach(eKey => {
181
- const eMsg = `<div class="fw-warning">${errors[eKey]}</div>`
182
- if ("error" == eKey) {
183
- document
184
- .getElementById("editimage")!
185
- .insertAdjacentHTML("afterbegin", eMsg)
186
- } else {
187
- const fieldEl = document.getElementById(`id_${eKey}`)
188
- if (fieldEl) {
189
- fieldEl.insertAdjacentHTML("afterend", eMsg)
190
- }
191
- }
192
- })
168
+ selectButton.addEventListener("click", () => {
169
+ mediaInputSelector.click();
170
+ });
171
+
172
+ const dialog = this.dialog;
173
+ if (!dialog) {
174
+ return;
193
175
  }
194
176
 
195
- async saveImage(): Promise<number | void> {
196
- const titleInput = document.querySelector(
197
- "#editimage .fw-media-title"
198
- ) as HTMLInputElement | null
199
- const imageData: SaveImageRequest = {
200
- title: titleInput ? titleInput.value : "",
201
- copyright: this.copyright,
202
- cats: this.catsList.value
203
- }
204
- if (this.imageId) {
205
- imageData.id = this.imageId
206
- } else if (!this.rotation && !this.cropped && this.mediaInput) {
207
- imageData.image = this.mediaInput
208
- } else if (this.mediaPreviewer && this.mediaInput) {
209
- const mediaPreviewer = this.mediaPreviewer as HTMLElement & {
210
- currentStyle?: CSSStyleDeclaration
211
- }
212
- const mediaPreviewerStyle =
213
- mediaPreviewer.currentStyle ||
214
- window.getComputedStyle(
215
- mediaPreviewer,
216
- false as unknown as string | null
217
- )
218
- const base64data = mediaPreviewerStyle.backgroundImage
219
- .slice(4, -1)
220
- .replace(/"/g, "")
221
- const bstr = atob(base64data.split(",")[1])
222
- let n = bstr.length
223
- const u8arr = new Uint8Array(n)
224
- while (n--) {
225
- u8arr[n] = bstr.charCodeAt(n)
226
- }
227
- imageData.image = new File([u8arr], this.mediaInput.name, {
228
- type: this.mediaInput.type
229
- })
230
- }
177
+ mediaInputSelector.addEventListener("change", () => {
178
+ this.mediaInput = mediaInputSelector.files![0];
179
+ const fr = new window.FileReader();
180
+ fr.onload = () => {
181
+ this.mediaPreviewerDiv!.innerHTML = `<div class="img" style="background-image: url(${fr.result});" />`;
182
+ this.mediaPreviewer =
183
+ this.mediaPreviewerDiv!.querySelector(".img") || undefined;
184
+ this.mediaPreviewerDiv!.classList.remove("crop-mode");
185
+ dialog.centerDialog();
186
+ };
187
+ fr.readAsDataURL(this.mediaInput);
188
+ });
189
+ }
231
190
 
232
- // For E2EE documents, encrypt the image and copyright before uploading
233
- const isE2EE = this.page.e2ee?.encrypted === true
234
- if (isE2EE && imageData.image) {
235
- imageData.image = await E2EEEncryptor.encryptImage(
236
- imageData.image as File | Blob,
237
- this.page.e2ee!.key
238
- )
239
- imageData.original_file_type = this.mediaInput?.type || "image/png"
240
- // Encrypt copyright metadata so the server cannot read it
241
- imageData.copyright = await E2EEEncryptor.encryptObject(
242
- imageData.copyright,
243
- this.page.e2ee!.key
244
- )
191
+ displayCreateImageError(errors: Record<string, string>): void {
192
+ Object.keys(errors).forEach((eKey) => {
193
+ const eMsg = `<div class="fw-warning">${errors[eKey]}</div>`;
194
+ if ("error" == eKey) {
195
+ document
196
+ .getElementById("editimage")!
197
+ .insertAdjacentHTML("afterbegin", eMsg);
198
+ } else {
199
+ const fieldEl = document.getElementById(`id_${eKey}`);
200
+ if (fieldEl) {
201
+ fieldEl.insertAdjacentHTML("afterend", eMsg);
245
202
  }
203
+ }
204
+ });
205
+ }
246
206
 
247
- // Remove old warning messages
248
- document
249
- .querySelectorAll("#editimage .fw-warning")
250
- .forEach(el => el.parentElement!.removeChild(el))
251
- return new Promise<number | void>(resolve => {
252
- const dialog = this.dialog
253
- this.imageDB.saveImage(imageData).then(
254
- imageId => {
255
- if (dialog) {
256
- dialog.close()
257
- }
258
- addAlert("success", gettext("The image has been updated."))
259
- this.imageId = imageId
260
- resolve(imageId)
261
- },
262
- errors => {
263
- if (this.page.app.isOffline()) {
264
- this.showOffline()
265
- return
266
- }
267
- this.displayCreateImageError(errors as Record<string, string>)
268
- addAlert(
269
- "error",
270
- gettext(
271
- "Some errors were found. Please examine the form."
272
- )
273
- )
274
- }
275
- )
276
- })
207
+ async saveImage(): Promise<number | void> {
208
+ const titleInput = document.querySelector(
209
+ "#editimage .fw-media-title",
210
+ ) as HTMLInputElement | null;
211
+ const imageData: SaveImageRequest = {
212
+ title: titleInput ? titleInput.value : "",
213
+ copyright: this.copyright,
214
+ cats: this.catsList.value,
215
+ };
216
+ if (this.imageId) {
217
+ imageData.id = this.imageId;
218
+ } else if (!this.rotation && !this.cropped && this.mediaInput) {
219
+ imageData.image = this.mediaInput;
220
+ } else if (this.mediaPreviewer && this.mediaInput) {
221
+ const mediaPreviewer = this.mediaPreviewer as HTMLElement & {
222
+ currentStyle?: CSSStyleDeclaration;
223
+ };
224
+ const mediaPreviewerStyle =
225
+ mediaPreviewer.currentStyle ||
226
+ window.getComputedStyle(
227
+ mediaPreviewer,
228
+ false as unknown as string | null,
229
+ );
230
+ const base64data = mediaPreviewerStyle.backgroundImage
231
+ .slice(4, -1)
232
+ .replace(/"/g, "");
233
+ const bstr = atob(base64data.split(",")[1]);
234
+ let n = bstr.length;
235
+ const u8arr = new Uint8Array(n);
236
+ while (n--) {
237
+ u8arr[n] = bstr.charCodeAt(n);
238
+ }
239
+ imageData.image = new File([u8arr], this.mediaInput.name, {
240
+ type: this.mediaInput.type,
241
+ });
277
242
  }
278
243
 
279
- showOffline(): void {
280
- addAlert(
281
- "info",
282
- gettext(
283
- "You are currently offline. Please try again after going online."
284
- )
285
- )
244
+ // For E2EE documents, encrypt the image and copyright before uploading
245
+ const isE2EE = this.page.e2ee?.encrypted === true;
246
+ if (isE2EE && imageData.image) {
247
+ imageData.image = await E2EEEncryptor.encryptImage(
248
+ imageData.image as File | Blob,
249
+ this.page.e2ee!.key,
250
+ );
251
+ imageData.original_file_type = this.mediaInput?.type || "image/png";
252
+ // Encrypt copyright metadata so the server cannot read it
253
+ imageData.copyright = await E2EEEncryptor.encryptObject(
254
+ imageData.copyright,
255
+ this.page.e2ee!.key,
256
+ );
286
257
  }
258
+
259
+ // Remove old warning messages
260
+ document
261
+ .querySelectorAll("#editimage .fw-warning")
262
+ .forEach((el) => el.parentElement!.removeChild(el));
263
+ return new Promise<number | void>((resolve) => {
264
+ const dialog = this.dialog;
265
+ this.imageDB.saveImage(imageData).then(
266
+ (imageId) => {
267
+ if (dialog) {
268
+ dialog.close();
269
+ }
270
+ addAlert("success", gettext("The image has been updated."));
271
+ this.imageId = imageId;
272
+ resolve(imageId);
273
+ },
274
+ (errors) => {
275
+ if (this.page.app.isOffline()) {
276
+ this.showOffline();
277
+ return;
278
+ }
279
+ this.displayCreateImageError(errors as Record<string, string>);
280
+ addAlert(
281
+ "error",
282
+ gettext("Some errors were found. Please examine the form."),
283
+ );
284
+ },
285
+ );
286
+ });
287
+ }
288
+
289
+ showOffline(): void {
290
+ addAlert(
291
+ "info",
292
+ gettext(
293
+ "You are currently offline. Please try again after going online.",
294
+ ),
295
+ );
296
+ }
287
297
  }