@shelamkoff/rector 1.0.1 → 1.0.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/README.md +25 -24
- package/dist/core/clipboard/Clipboard.d.ts +2 -0
- package/dist/core/clipboard/Clipboard.js +52 -24
- package/dist/core/index.js +195 -194
- package/dist/core/locale/en.d.ts +1 -0
- package/dist/core/locale/en.js +1 -0
- package/dist/core/locale/ru.d.ts +1 -0
- package/dist/core/locale/ru.js +1 -0
- package/dist/core/themes/light.css +1 -1
- package/dist/core/themes/variables.css +72 -10
- package/dist/locale/en.d.ts +41 -0
- package/dist/locale/ru.d.ts +41 -0
- package/dist/plugins/attaches/attaches.css +16 -14
- package/dist/plugins/attaches/index.js +997 -920
- package/dist/plugins/attaches/locale/en.d.ts +9 -0
- package/dist/plugins/attaches/locale/en.js +11 -2
- package/dist/plugins/attaches/locale/ru.d.ts +9 -0
- package/dist/plugins/attaches/locale/ru.js +11 -2
- package/dist/plugins/carousel/carousel.css +213 -49
- package/dist/plugins/carousel/index.js +918 -858
- package/dist/plugins/carousel/locale/en.d.ts +15 -0
- package/dist/plugins/carousel/locale/en.js +18 -3
- package/dist/plugins/carousel/locale/ru.d.ts +15 -0
- package/dist/plugins/carousel/locale/ru.js +18 -3
- package/dist/plugins/gallery/README.md +92 -92
- package/dist/plugins/gallery/README.ru.md +17 -17
- package/dist/plugins/gallery/gallery.css +44 -19
- package/dist/plugins/gallery/index.js +375 -354
- package/dist/plugins/gallery/locale/en.d.ts +8 -0
- package/dist/plugins/gallery/locale/en.js +11 -3
- package/dist/plugins/gallery/locale/ru.d.ts +8 -0
- package/dist/plugins/gallery/locale/ru.js +11 -3
- package/dist/plugins/gallery/settings.js +15 -10
- package/dist/plugins/gallery/view-empty.d.ts +2 -0
- package/dist/plugins/gallery/view-empty.js +21 -15
- package/dist/plugins/gallery/view-filled.d.ts +2 -2
- package/dist/plugins/gallery/view-filled.js +84 -72
- package/dist/plugins/image/image.css +7 -2
- package/dist/plugins/image/index.js +365 -345
- package/dist/plugins/image/locale/en.d.ts +8 -0
- package/dist/plugins/image/locale/en.js +11 -3
- package/dist/plugins/image/locale/ru.d.ts +8 -0
- package/dist/plugins/image/locale/ru.js +11 -3
- package/dist/plugins/image/view-empty.d.ts +2 -0
- package/dist/plugins/image/view-empty.js +21 -15
- package/dist/plugins/image/view-filled.d.ts +2 -2
- package/dist/plugins/image/view-filled.js +77 -72
- package/dist/plugins/shared/dropzone.d.ts +6 -0
- package/dist/plugins/shared/dropzone.js +47 -36
- package/dist/plugins/shared/layer.d.ts +13 -0
- package/dist/plugins/shared/layer.js +45 -0
- package/dist/plugins/shared/sourceEditor.css +165 -0
- package/dist/plugins/shared/sourceEditor.d.ts +97 -0
- package/dist/plugins/shared/sourceEditor.js +260 -0
- package/dist/renderer/renderers/carousel/index.js +24 -23
- package/dist/renderer/renderers/carousel/styles.css +8 -2
- package/dist/renderer/renderers/gallery/README.md +41 -41
- package/dist/renderer/renderers/gallery/README.ru.md +41 -41
- package/dist/renderer/renderers/gallery/index.js +100 -85
- package/dist/renderer/renderers/gallery/styles.css +417 -401
- package/dist/shared/galleryMasonry.d.ts +34 -0
- package/dist/shared/galleryMasonry.js +152 -0
- package/package.json +29 -28
|
@@ -1,354 +1,375 @@
|
|
|
1
|
-
import { isSupportedImageFile, triggerFileInput } from '../shared/fileInput.js'
|
|
2
|
-
import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
|
|
3
|
-
import { validateGalleryData } from '../../shared/blockDataValidators.js'
|
|
4
|
-
import { CSS } from './css.js'
|
|
5
|
-
import { ICON } from './icons.js'
|
|
6
|
-
import { GalleryState, normalizeGalleryData, emptyGalleryData } from './state.js'
|
|
7
|
-
import { GalleryUploader } from './uploader.js'
|
|
8
|
-
import { renderEmptyView } from './view-empty.js'
|
|
9
|
-
import { renderFilledView } from './view-filled.js'
|
|
10
|
-
import { sanitizeMediaUrl } from '../../shared/sanitize/sanitizeUrl.js'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @typedef {
|
|
18
|
-
*
|
|
19
|
-
* @
|
|
20
|
-
* @property {
|
|
21
|
-
* @property {string}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* - `
|
|
30
|
-
* - `
|
|
31
|
-
* - `
|
|
32
|
-
* - `
|
|
33
|
-
* - `
|
|
34
|
-
*
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
/**
|
|
52
|
-
#
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
* @
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
wrapper
|
|
85
|
-
wrapper.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this.#
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.#
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
*
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
state.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1
|
+
import { isSupportedImageFile, triggerFileInput } from '../shared/fileInput.js'
|
|
2
|
+
import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
|
|
3
|
+
import { validateGalleryData } from '../../shared/blockDataValidators.js'
|
|
4
|
+
import { CSS } from './css.js'
|
|
5
|
+
import { ICON } from './icons.js'
|
|
6
|
+
import { GalleryState, normalizeGalleryData, emptyGalleryData } from './state.js'
|
|
7
|
+
import { GalleryUploader } from './uploader.js'
|
|
8
|
+
import { renderEmptyView } from './view-empty.js'
|
|
9
|
+
import { renderFilledView } from './view-filled.js'
|
|
10
|
+
import { sanitizeMediaUrl } from '../../shared/sanitize/sanitizeUrl.js'
|
|
11
|
+
import { openSourceEditor, preloadSourceEditor } from '../shared/sourceEditor.js'
|
|
12
|
+
|
|
13
|
+
const editorStyles = new URL('./gallery.css', import.meta.url).href
|
|
14
|
+
const sourceEditorStyles = new URL('../shared/sourceEditor.css', import.meta.url).href
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {(file: File, context: { signal: AbortSignal }) => Promise<{ url: string, alt?: string }>} UploadFn
|
|
18
|
+
*
|
|
19
|
+
* @typedef {Object} GalleryConfig
|
|
20
|
+
* @property {UploadFn} [uploadFile] Uploads one browser file. Without this callback the plugin reads each file into a data URL stored in the document.
|
|
21
|
+
* @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null> }>} [actions] Additional application-owned image sources. `icon` is trusted application markup; never pass user-authored HTML.
|
|
22
|
+
* @property {boolean} [injectStyles=true] Whether the editor should load the built-in gallery stylesheet.
|
|
23
|
+
* @property {string} [css] Additional stylesheet URL, or the replacement URL when `injectStyles` is `false`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Editable image gallery with upload-source extensions, ordering, and layout controls.
|
|
28
|
+
* Internal logic is split across:
|
|
29
|
+
* - `state.js` — per-block state container (replaces module WeakMap)
|
|
30
|
+
* - `uploader.js` — multi-file upload pipeline
|
|
31
|
+
* - `layout.js` — layout selection algorithms
|
|
32
|
+
* - `slot.js` — slot/overflow item DOM builders + drag handling
|
|
33
|
+
* - `view-empty.js`/ `view-filled.js` — DOM rendering for the two states
|
|
34
|
+
* - `settings.js` — settings dropdown form
|
|
35
|
+
* - `styles.js` — gallery-level inline style application
|
|
36
|
+
* @extends {BlockPluginAbstract<GalleryConfig>}
|
|
37
|
+
*/
|
|
38
|
+
export class Gallery extends BlockPluginAbstract {
|
|
39
|
+
static isTextBlock = false
|
|
40
|
+
static styles = [editorStyles, sourceEditorStyles]
|
|
41
|
+
|
|
42
|
+
type = 'gallery'
|
|
43
|
+
icon = ICON
|
|
44
|
+
inlineTools = false
|
|
45
|
+
|
|
46
|
+
pasteConfig = {
|
|
47
|
+
files: ['image/*'],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#uploader
|
|
51
|
+
/** Per-block state, encapsulated to this plugin instance. */
|
|
52
|
+
#states = /** @type {WeakMap<HTMLElement, GalleryState>} */ (new WeakMap())
|
|
53
|
+
/** @type {WeakMap<HTMLElement, import('../../core/types').BlockMutationContext>} */
|
|
54
|
+
#contexts = new WeakMap()
|
|
55
|
+
/**
|
|
56
|
+
* Create a Gallery instance with the supplied consumer configuration.
|
|
57
|
+
* @param {GalleryConfig} [config]
|
|
58
|
+
*/
|
|
59
|
+
constructor(config) {
|
|
60
|
+
super(config)
|
|
61
|
+
this.#uploader = new GalleryUploader(this._config)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Return the localized toolbox label for this block.
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
get title() {
|
|
69
|
+
return this._t('title', 'Gallery')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── BlockPlugin contract ───────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create the editable DOM owned by this block instance.
|
|
76
|
+
* @param {Record<string, unknown>} data
|
|
77
|
+
* @param {import('../../core/types').BlockMutationContext} context
|
|
78
|
+
* @returns {HTMLElement}
|
|
79
|
+
*/
|
|
80
|
+
render(data, context) {
|
|
81
|
+
const blockData = normalizeGalleryData(data)
|
|
82
|
+
const pendingFile = /** @type {File | null} */ (/** @type {any} */ (data)?._pendingFile || null)
|
|
83
|
+
|
|
84
|
+
const wrapper = document.createElement('div')
|
|
85
|
+
wrapper.classList.add(CSS.wrapper)
|
|
86
|
+
wrapper.contentEditable = 'false'
|
|
87
|
+
wrapper.tabIndex = -1
|
|
88
|
+
|
|
89
|
+
const state = new GalleryState(blockData, pendingFile ? [pendingFile] : [])
|
|
90
|
+
this.#states.set(wrapper, state)
|
|
91
|
+
this.#contexts.set(wrapper, context)
|
|
92
|
+
|
|
93
|
+
if (blockData.images.length > 0) {
|
|
94
|
+
this.#renderFilled(wrapper)
|
|
95
|
+
} else {
|
|
96
|
+
this.#renderEmpty(wrapper)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (state.pendingFiles.length > 0 && !context.readOnly) {
|
|
100
|
+
const files = state.pendingFiles
|
|
101
|
+
state.pendingFiles = []
|
|
102
|
+
state.pendingUpload = this.#handleFiles(wrapper, files).finally(() => {
|
|
103
|
+
if (this.#states.get(wrapper) === state) state.pendingUpload = null
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return wrapper
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serialize the current block DOM into document data.
|
|
112
|
+
* @param {HTMLElement} element
|
|
113
|
+
* @returns {Record<string, unknown>}
|
|
114
|
+
*/
|
|
115
|
+
save(element) {
|
|
116
|
+
const state = this.#states.get(element)
|
|
117
|
+
if (!state) return emptyGalleryData()
|
|
118
|
+
this.#syncCaptions(element)
|
|
119
|
+
return {
|
|
120
|
+
images: state.data.images.map((img) => ({ ...img })),
|
|
121
|
+
layout: state.data.layout,
|
|
122
|
+
styles: { ...state.data.styles },
|
|
123
|
+
options: { ...state.data.options },
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check whether serialized data satisfies this block's schema.
|
|
129
|
+
* @param {Record<string, unknown>} data
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
validate(data) {
|
|
133
|
+
return validateGalleryData(data)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check whether the block has no meaningful user content.
|
|
138
|
+
* @param {HTMLElement} element
|
|
139
|
+
* @returns {boolean}
|
|
140
|
+
*/
|
|
141
|
+
isEmpty(element) {
|
|
142
|
+
const state = this.#states.get(element)
|
|
143
|
+
return !state || state.data.images.length === 0
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract neutral text that can initialize another block type.
|
|
148
|
+
* @param {HTMLElement} element
|
|
149
|
+
* @returns {Record<string, unknown>}
|
|
150
|
+
*/
|
|
151
|
+
exportData(element) {
|
|
152
|
+
const state = this.#states.get(element)
|
|
153
|
+
return { text: state?.data.images.map(image => image.caption).filter(Boolean).join(' ') || '' }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle supported pasted content for this block.
|
|
158
|
+
* @param {import('../../types').PasteEvent} event
|
|
159
|
+
* @returns {Record<string, unknown> | null}
|
|
160
|
+
*/
|
|
161
|
+
onPaste(event) {
|
|
162
|
+
if (event.type === 'file') {
|
|
163
|
+
// Files dropped via paste — push immediately into the new block.
|
|
164
|
+
// Render path will then upload via the wrapper bound to this default data.
|
|
165
|
+
const data = /** @type {any} */ (emptyGalleryData())
|
|
166
|
+
data._pendingFile = event.file
|
|
167
|
+
return data
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Keep pasted files inside one undo transaction until upload completes.
|
|
174
|
+
* @param {HTMLElement} element
|
|
175
|
+
* @returns {Promise<void>}
|
|
176
|
+
*/
|
|
177
|
+
waitForPaste(element) {
|
|
178
|
+
return this.#states.get(element)?.pendingUpload ?? Promise.resolve()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Release listeners and resources owned by this block element.
|
|
183
|
+
* @param {HTMLElement} element
|
|
184
|
+
* @returns {void}
|
|
185
|
+
*/
|
|
186
|
+
destroy(element) {
|
|
187
|
+
const state = this.#states.get(element)
|
|
188
|
+
if (!state) return
|
|
189
|
+
state.dispose()
|
|
190
|
+
this.#states.delete(element)
|
|
191
|
+
this.#contexts.delete(element)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Internal coordination ──────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/** @param {string} key @param {string} fallback @returns {string} */
|
|
197
|
+
#t = (key, fallback) => this._t(key, fallback)
|
|
198
|
+
|
|
199
|
+
/** @param {HTMLElement} wrapper @param {() => void} operation @returns {void} */
|
|
200
|
+
#mutate = (wrapper, operation) => {
|
|
201
|
+
this.#contexts.get(wrapper)?.mutate(operation)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Flush in-flight contenteditable caption text into state before any
|
|
206
|
+
* mutation that would re-render and tear down the live caption nodes.
|
|
207
|
+
*
|
|
208
|
+
* @param {HTMLElement} wrapper
|
|
209
|
+
* @returns {void}
|
|
210
|
+
*/
|
|
211
|
+
#syncCaptions(wrapper) {
|
|
212
|
+
const state = this.#states.get(wrapper)
|
|
213
|
+
if (!state) return
|
|
214
|
+
const slots = wrapper.querySelectorAll(`.${CSS.slot}.${CSS.slotFilled}`)
|
|
215
|
+
slots.forEach((slotEl) => {
|
|
216
|
+
const slot = /** @type {HTMLElement} */ (slotEl)
|
|
217
|
+
const idx = parseInt(slot.dataset.slot || slot.dataset.index || '0', 10)
|
|
218
|
+
const img = state.data.images[idx]
|
|
219
|
+
if (img) {
|
|
220
|
+
const caption = slot.querySelector(`.${CSS.slotCaption}`)
|
|
221
|
+
img.caption = caption?.textContent?.trim() || ''
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
227
|
+
#renderEmpty(wrapper) {
|
|
228
|
+
const state = this.#states.get(wrapper)
|
|
229
|
+
if (!state) return
|
|
230
|
+
const readOnly = Boolean(this.#contexts.get(wrapper)?.readOnly)
|
|
231
|
+
renderEmptyView(wrapper, state, {
|
|
232
|
+
t: this.#t,
|
|
233
|
+
readOnly,
|
|
234
|
+
onUploadClick: () => this.#triggerFileInput(wrapper),
|
|
235
|
+
onOpenUrlEditor: () => this.#openUrlEditor(wrapper),
|
|
236
|
+
onFilesDropped: (files) => { void this.#handleFiles(wrapper, files) },
|
|
237
|
+
customActions: this._config.actions || [],
|
|
238
|
+
runCustomAction: async (handler) => this.#runCustomAction(wrapper, handler),
|
|
239
|
+
})
|
|
240
|
+
if (!readOnly) preloadSourceEditor(wrapper, state.abortController.signal, ['url'])
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
244
|
+
#renderFilled(wrapper) {
|
|
245
|
+
const state = this.#states.get(wrapper)
|
|
246
|
+
if (!state) return
|
|
247
|
+
const readOnly = Boolean(this.#contexts.get(wrapper)?.readOnly)
|
|
248
|
+
renderFilledView(wrapper, state, {
|
|
249
|
+
t: this.#t,
|
|
250
|
+
readOnly,
|
|
251
|
+
syncCaptions: () => this.#syncCaptions(wrapper),
|
|
252
|
+
getState: () => this.#states.get(wrapper),
|
|
253
|
+
reRender: () => this.#renderFilled(wrapper),
|
|
254
|
+
renderEmpty: () => this.#renderEmpty(wrapper),
|
|
255
|
+
mutate: (operation) => this.#mutate(wrapper, operation),
|
|
256
|
+
onFilesDropped: (files) => { void this.#handleFiles(wrapper, files) },
|
|
257
|
+
onTriggerFileInput: () => this.#triggerFileInput(wrapper),
|
|
258
|
+
onOpenUrlEditor: () => this.#openUrlEditor(wrapper),
|
|
259
|
+
onDeleteAll: () => this.#deleteAll(wrapper),
|
|
260
|
+
customActions: this._config.actions || [],
|
|
261
|
+
runCustomAction: async (handler) => this.#runCustomAction(wrapper, handler),
|
|
262
|
+
})
|
|
263
|
+
if (!readOnly) preloadSourceEditor(wrapper, state.abortController.signal, ['url'])
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
267
|
+
#triggerFileInput(wrapper) {
|
|
268
|
+
if (this.#contexts.get(wrapper)?.readOnly) return
|
|
269
|
+
triggerFileInput({
|
|
270
|
+
accept: 'image/*',
|
|
271
|
+
multiple: true,
|
|
272
|
+
signal: this.#states.get(wrapper)?.abortController?.signal,
|
|
273
|
+
onFiles: (files) => void this.#handleFiles(wrapper, files),
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {HTMLElement} wrapper
|
|
279
|
+
* @param {File[]} files
|
|
280
|
+
* @returns {Promise<void>}
|
|
281
|
+
*/
|
|
282
|
+
async #handleFiles(wrapper, files) {
|
|
283
|
+
const state = this.#states.get(wrapper)
|
|
284
|
+
const context = this.#contexts.get(wrapper)
|
|
285
|
+
if (!state || !context || context.readOnly) return
|
|
286
|
+
const accepted = files.filter(isSupportedImageFile)
|
|
287
|
+
if (accepted.length === 0) return
|
|
288
|
+
const controller = state.beginTask()
|
|
289
|
+
wrapper.classList.add(CSS.loading)
|
|
290
|
+
try {
|
|
291
|
+
await this.#uploader.handle(accepted, (added) => {
|
|
292
|
+
if (controller.signal.aborted || this.#states.get(wrapper) !== state) return
|
|
293
|
+
this.#mutate(wrapper, () => {
|
|
294
|
+
this.#syncCaptions(wrapper)
|
|
295
|
+
state.data.images.push(...added)
|
|
296
|
+
this.#renderFilled(wrapper)
|
|
297
|
+
})
|
|
298
|
+
}, controller.signal)
|
|
299
|
+
} finally {
|
|
300
|
+
if (state.finishTask(controller)) wrapper.classList.remove(CSS.loading)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {HTMLElement} wrapper
|
|
306
|
+
* @param {(context: { signal: AbortSignal }) => Promise<Array<{url: string, alt?: string}> | null>} handler
|
|
307
|
+
* @returns {Promise<void>}
|
|
308
|
+
*/
|
|
309
|
+
async #runCustomAction(wrapper, handler) {
|
|
310
|
+
const state = this.#states.get(wrapper)
|
|
311
|
+
if (!state || this.#contexts.get(wrapper)?.readOnly) return
|
|
312
|
+
const controller = state.beginTask()
|
|
313
|
+
wrapper.classList.add(CSS.loading)
|
|
314
|
+
try {
|
|
315
|
+
const result = await handler({ signal: controller.signal })
|
|
316
|
+
if (!controller.signal.aborted && this.#states.get(wrapper) === state && Array.isArray(result) && result.length > 0) {
|
|
317
|
+
const added = result.flatMap(item => {
|
|
318
|
+
const url = sanitizeMediaUrl(item?.url || '')
|
|
319
|
+
if (!url) return []
|
|
320
|
+
return [{ url, caption: typeof item?.alt === 'string' ? item.alt : '' }]
|
|
321
|
+
})
|
|
322
|
+
if (!added.length) return
|
|
323
|
+
this.#mutate(wrapper, () => {
|
|
324
|
+
this.#syncCaptions(wrapper)
|
|
325
|
+
state.data.images.push(...added)
|
|
326
|
+
this.#renderFilled(wrapper)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
// Action cancelled or failed.
|
|
331
|
+
} finally {
|
|
332
|
+
if (state.finishTask(controller)) wrapper.classList.remove(CSS.loading)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
337
|
+
#openUrlEditor(wrapper) {
|
|
338
|
+
const state = this.#states.get(wrapper)
|
|
339
|
+
const signal = state?.abortController?.signal
|
|
340
|
+
if (!state || !signal || this.#contexts.get(wrapper)?.readOnly) return
|
|
341
|
+
openSourceEditor({
|
|
342
|
+
wrapper,
|
|
343
|
+
signal,
|
|
344
|
+
kind: 'url',
|
|
345
|
+
title: this.#t('urlEditorTitle', 'Insert image by URL'),
|
|
346
|
+
label: this.#t('urlEditorLabel', 'Image URL'),
|
|
347
|
+
placeholder: this.#t('urlEditorPlaceholder', 'https://example.com/image.jpg'),
|
|
348
|
+
submitText: this.#t('sourceSubmit', 'Insert'),
|
|
349
|
+
cancelText: this.#t('sourceCancel', 'Cancel'),
|
|
350
|
+
invalidText: this.#t('invalidUrl', 'Enter a valid image URL.'),
|
|
351
|
+
normalize: sanitizeMediaUrl,
|
|
352
|
+
onSubmit: (url) => {
|
|
353
|
+
const current = this.#states.get(wrapper)
|
|
354
|
+
if (current !== state || this.#contexts.get(wrapper)?.readOnly) return
|
|
355
|
+
this.#mutate(wrapper, () => {
|
|
356
|
+
this.#syncCaptions(wrapper)
|
|
357
|
+
state.data.images.push({ url, caption: '' })
|
|
358
|
+
this.#renderFilled(wrapper)
|
|
359
|
+
})
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
365
|
+
#deleteAll(wrapper) {
|
|
366
|
+
const state = this.#states.get(wrapper)
|
|
367
|
+
if (!state) return
|
|
368
|
+
state.cancelTasks()
|
|
369
|
+
wrapper.classList.remove(CSS.loading)
|
|
370
|
+
this.#mutate(wrapper, () => {
|
|
371
|
+
state.data.images = []
|
|
372
|
+
this.#renderEmpty(wrapper)
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
}
|