@shelamkoff/rector 1.0.0 → 1.0.2
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 -23
- 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/inline-plugins/mention/index.js +1 -2
- 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 -921
- 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 -859
- 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/checklist/index.js +1 -2
- package/dist/plugins/code/index.js +1 -2
- package/dist/plugins/columns/index.js +1 -2
- package/dist/plugins/delimiter/index.js +1 -2
- package/dist/plugins/embed/index.js +1 -2
- package/dist/plugins/gallery/gallery.css +6 -1
- package/dist/plugins/gallery/index.js +375 -355
- 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/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 +61 -56
- package/dist/plugins/heading/index.js +1 -2
- package/dist/plugins/image/image.css +7 -2
- package/dist/plugins/image/index.js +365 -346
- 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/link-preview/index.js +1 -2
- package/dist/plugins/list/index.js +1 -2
- package/dist/plugins/paragraph/index.js +1 -2
- package/dist/plugins/person/index.js +1 -2
- package/dist/plugins/poll/index.js +1 -2
- package/dist/plugins/quote/index.js +1 -2
- package/dist/plugins/raw/index.js +1 -2
- 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/plugins/spoiler/index.js +1 -2
- package/dist/plugins/table/index.js +1 -2
- package/dist/plugins/toggle/index.js +1 -2
- package/dist/plugins/warning/index.js +1 -2
- package/dist/renderer/index.js +1 -2
- package/dist/renderer/renderers/attaches/index.js +1 -2
- package/dist/renderer/renderers/carousel/index.js +24 -24
- package/dist/renderer/renderers/carousel/styles.css +8 -2
- package/dist/renderer/renderers/checklist/index.js +1 -2
- package/dist/renderer/renderers/code/index.js +1 -2
- package/dist/renderer/renderers/columns/index.js +1 -2
- package/dist/renderer/renderers/delimiter/index.js +1 -2
- package/dist/renderer/renderers/embed/index.js +1 -2
- package/dist/renderer/renderers/gallery/index.js +1 -2
- package/dist/renderer/renderers/heading/index.js +1 -2
- package/dist/renderer/renderers/image/index.js +1 -2
- package/dist/renderer/renderers/link-preview/index.js +1 -2
- package/dist/renderer/renderers/list/index.js +1 -2
- package/dist/renderer/renderers/paragraph/index.js +1 -2
- package/dist/renderer/renderers/person/index.js +1 -2
- package/dist/renderer/renderers/poll/index.js +1 -2
- package/dist/renderer/renderers/quote/index.js +1 -2
- package/dist/renderer/renderers/spoiler/index.js +1 -2
- package/dist/renderer/renderers/table/index.js +1 -2
- package/dist/renderer/renderers/toggle/index.js +1 -2
- package/dist/renderer/renderers/warning/index.js +1 -2
- package/package.json +27 -29
- package/dist/shared/resolvePath.d.ts +0 -8
- package/dist/shared/resolvePath.js +0 -10
|
@@ -1,346 +1,365 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
const editorStyles =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @typedef {
|
|
18
|
-
* @typedef {(context: { signal: AbortSignal }) => Promise<ImageSourceResult
|
|
19
|
-
* @typedef {{
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
23
|
-
* @property {
|
|
24
|
-
* @property {
|
|
25
|
-
* @property {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* - `
|
|
33
|
-
* - `
|
|
34
|
-
* - `
|
|
35
|
-
* - `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
static
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/** @type {WeakMap<HTMLElement,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
* @param {
|
|
83
|
-
* @
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
wrapper.
|
|
91
|
-
wrapper.
|
|
92
|
-
wrapper.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (blockData.
|
|
100
|
-
if (blockData.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
state.pendingFile
|
|
112
|
-
state.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
data
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
*
|
|
184
|
-
* @
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
*
|
|
193
|
-
* @
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
state
|
|
199
|
-
|
|
200
|
-
this.#
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
*
|
|
208
|
-
* @param {string}
|
|
209
|
-
* @
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
*
|
|
216
|
-
* @param {
|
|
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
|
-
}
|
|
1
|
+
import { isSupportedImageFile, triggerFileInput } from '../shared/fileInput.js'
|
|
2
|
+
import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
|
|
3
|
+
import { validateImageData } from '../../shared/blockDataValidators.js'
|
|
4
|
+
import { CSS } from './css.js'
|
|
5
|
+
import { ICON } from './icons.js'
|
|
6
|
+
import { ImageState, normalizeImageData, emptyImageData } from './state.js'
|
|
7
|
+
import { ImageUploader } from './uploader.js'
|
|
8
|
+
import { renderEmptyView } from './view-empty.js'
|
|
9
|
+
import { renderFilledView } from './view-filled.js'
|
|
10
|
+
import { sanitizeMediaUrl } from '../../shared/sanitize/index.js'
|
|
11
|
+
import { openSourceEditor, preloadSourceEditor } from '../shared/sourceEditor.js'
|
|
12
|
+
|
|
13
|
+
const editorStyles = new URL('./image.css', import.meta.url).href
|
|
14
|
+
const sourceEditorStyles = new URL('../shared/sourceEditor.css', import.meta.url).href
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{ url: string, alt?: string }} ImageSourceResult
|
|
18
|
+
* @typedef {(file: File, context: { signal: AbortSignal }) => Promise<ImageSourceResult>} ImageUpload
|
|
19
|
+
* @typedef {(context: { signal: AbortSignal }) => Promise<ImageSourceResult | null>} ImageSourceHandler
|
|
20
|
+
* @typedef {{ icon?: string, label: string, handler: ImageSourceHandler }} ImageSourceAction
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} ImageConfig
|
|
23
|
+
* @property {ImageUpload} [uploadFile] Uploads a browser file. Without this callback the plugin reads the file into a data URL stored in the document.
|
|
24
|
+
* @property {ImageSourceAction[]} [actions] Additional application-owned image sources. `icon` is trusted application markup; never pass user-authored HTML.
|
|
25
|
+
* @property {boolean} [injectStyles=true] Whether the editor should load the built-in image stylesheet.
|
|
26
|
+
* @property {string} [css] Additional stylesheet URL, or the replacement URL when `injectStyles` is `false`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Block plugin for images. Public surface implements `BlockPlugin`.
|
|
31
|
+
* Internal logic is split across:
|
|
32
|
+
* - `state.js` — per-block state container (replaces module WeakMap)
|
|
33
|
+
* - `uploader.js` — file upload pipeline (HTTP or data-URL fallback)
|
|
34
|
+
* - `view-empty.js`/`view-filled.js` — DOM rendering for the two states
|
|
35
|
+
* - `settings.js` — settings dropdown form
|
|
36
|
+
* - `styles.js` — inline style application
|
|
37
|
+
*
|
|
38
|
+
* @extends {BlockPluginAbstract<ImageConfig>}
|
|
39
|
+
*/
|
|
40
|
+
export class Image extends BlockPluginAbstract {
|
|
41
|
+
static isTextBlock = false
|
|
42
|
+
static styles = [editorStyles, sourceEditorStyles]
|
|
43
|
+
|
|
44
|
+
type = 'image'
|
|
45
|
+
icon = ICON
|
|
46
|
+
inlineTools = false
|
|
47
|
+
|
|
48
|
+
pasteConfig = {
|
|
49
|
+
files: ['image/*'],
|
|
50
|
+
patterns: [/https?:\/\/\S+\.(gif|jpe?g|png|svg|webp)(\?\S*)?$/i],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#uploader
|
|
54
|
+
/** @type {ImageConfig} */
|
|
55
|
+
#config
|
|
56
|
+
/** Per-block state, keyed by wrapper element. Encapsulated to this instance. */
|
|
57
|
+
#states = /** @type {WeakMap<HTMLElement, ImageState>} */ (new WeakMap())
|
|
58
|
+
/** @type {WeakMap<HTMLElement, import('../../core/types').BlockMutationContext>} */
|
|
59
|
+
#contexts = new WeakMap()
|
|
60
|
+
/**
|
|
61
|
+
* Create an Image instance with the supplied consumer configuration.
|
|
62
|
+
* @param {ImageConfig} [config]
|
|
63
|
+
*/
|
|
64
|
+
constructor(config) {
|
|
65
|
+
super(config)
|
|
66
|
+
this.#config = /** @type {ImageConfig} */ (this.getPluginConfig())
|
|
67
|
+
this.#uploader = new ImageUploader(this.#config)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Return the localized toolbox label for this block.
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
get title() {
|
|
75
|
+
return this._t('title', 'Image')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── BlockPlugin contract ───────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create the editable DOM owned by this block instance.
|
|
82
|
+
* @param {Record<string, unknown>} data
|
|
83
|
+
* @param {import('../../core/types').BlockMutationContext} context
|
|
84
|
+
* @returns {HTMLElement}
|
|
85
|
+
*/
|
|
86
|
+
render(data, context) {
|
|
87
|
+
const blockData = normalizeImageData(data)
|
|
88
|
+
const pendingFile = /** @type {File | null} */ (/** @type {any} */ (data)?._pendingFile || null)
|
|
89
|
+
|
|
90
|
+
const wrapper = document.createElement('div')
|
|
91
|
+
wrapper.classList.add(CSS.wrapper)
|
|
92
|
+
wrapper.contentEditable = 'false'
|
|
93
|
+
wrapper.tabIndex = -1
|
|
94
|
+
|
|
95
|
+
const state = new ImageState(blockData, pendingFile)
|
|
96
|
+
this.#states.set(wrapper, state)
|
|
97
|
+
this.#contexts.set(wrapper, context)
|
|
98
|
+
|
|
99
|
+
if (blockData.withBorder) wrapper.classList.add(CSS.withBorder)
|
|
100
|
+
if (blockData.expanded) wrapper.classList.add(CSS.expanded)
|
|
101
|
+
if (blockData.withBackground) wrapper.classList.add(CSS.withBackground)
|
|
102
|
+
|
|
103
|
+
if (blockData.file.url) {
|
|
104
|
+
this.#renderFilled(wrapper)
|
|
105
|
+
} else {
|
|
106
|
+
this.#renderEmpty(wrapper)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Drain pending file from paste (async; renders after upload completes).
|
|
110
|
+
if (state.pendingFile && !context.readOnly) {
|
|
111
|
+
const file = state.pendingFile
|
|
112
|
+
state.pendingFile = null
|
|
113
|
+
state.pendingUpload = this.#handleFile(wrapper, file).finally(() => {
|
|
114
|
+
if (this.#states.get(wrapper) === state) state.pendingUpload = null
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return wrapper
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Serialize the current block DOM into document data.
|
|
123
|
+
* @param {HTMLElement} element
|
|
124
|
+
* @returns {Record<string, unknown>}
|
|
125
|
+
*/
|
|
126
|
+
save(element) {
|
|
127
|
+
const state = this.#states.get(element)
|
|
128
|
+
if (!state) return emptyImageData()
|
|
129
|
+
return { ...state.data, styles: { ...state.data.styles } }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check whether serialized data satisfies this block's schema.
|
|
134
|
+
* @param {Record<string, unknown>} data
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
validate(data) {
|
|
138
|
+
return validateImageData(data)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check whether the block has no meaningful user content.
|
|
143
|
+
* @param {HTMLElement} element
|
|
144
|
+
* @returns {boolean}
|
|
145
|
+
*/
|
|
146
|
+
isEmpty(element) {
|
|
147
|
+
const state = this.#states.get(element)
|
|
148
|
+
return !state?.data.file.url
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract neutral text that can initialize another block type.
|
|
153
|
+
* @param {HTMLElement} element
|
|
154
|
+
* @returns {Record<string, unknown>}
|
|
155
|
+
*/
|
|
156
|
+
exportData(element) {
|
|
157
|
+
const state = this.#states.get(element)
|
|
158
|
+
return { text: state?.data.caption || '' }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle supported pasted content for this block.
|
|
163
|
+
* @param {import('../../types').PasteEvent} event
|
|
164
|
+
* @returns {Record<string, unknown> | null}
|
|
165
|
+
*/
|
|
166
|
+
onPaste(event) {
|
|
167
|
+
if (event.type === 'file') {
|
|
168
|
+
// Pass the file through the _pendingFile marker; render() will pick it up.
|
|
169
|
+
const data = /** @type {any} */ (emptyImageData())
|
|
170
|
+
data._pendingFile = event.file
|
|
171
|
+
return data
|
|
172
|
+
}
|
|
173
|
+
if (event.type === 'pattern') {
|
|
174
|
+
const url = String(event.data)
|
|
175
|
+
if (url && /^https?:\/\/.+/i.test(url)) {
|
|
176
|
+
return { ...emptyImageData(), file: { url } }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Keep a file paste inside one undo transaction until its upload finishes.
|
|
184
|
+
* @param {HTMLElement} element
|
|
185
|
+
* @returns {Promise<void>}
|
|
186
|
+
*/
|
|
187
|
+
waitForPaste(element) {
|
|
188
|
+
return this.#states.get(element)?.pendingUpload ?? Promise.resolve()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Release listeners and resources owned by this block element.
|
|
193
|
+
* @param {HTMLElement} element
|
|
194
|
+
* @returns {void}
|
|
195
|
+
*/
|
|
196
|
+
destroy(element) {
|
|
197
|
+
const state = this.#states.get(element)
|
|
198
|
+
if (!state) return
|
|
199
|
+
state.dispose()
|
|
200
|
+
this.#states.delete(element)
|
|
201
|
+
this.#contexts.delete(element)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Internal coordination ──────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve a localized image-plugin message.
|
|
208
|
+
* @param {string} key Translation key scoped to the image plugin.
|
|
209
|
+
* @param {string} fallback Message returned when the key is unavailable.
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
#t = (key, fallback) => this._t(key, fallback)
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Execute one completed block mutation through the editor command context.
|
|
216
|
+
* @param {HTMLElement} wrapper Image block wrapper that owns the context.
|
|
217
|
+
* @param {() => void} operation Synchronous DOM and state mutation.
|
|
218
|
+
* @returns {void}
|
|
219
|
+
*/
|
|
220
|
+
#mutate = (wrapper, operation) => {
|
|
221
|
+
this.#contexts.get(wrapper)?.mutate(operation)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
225
|
+
#renderEmpty(wrapper) {
|
|
226
|
+
const state = this.#states.get(wrapper)
|
|
227
|
+
if (!state) return
|
|
228
|
+
const readOnly = Boolean(this.#contexts.get(wrapper)?.readOnly)
|
|
229
|
+
renderEmptyView(wrapper, state, {
|
|
230
|
+
t: this.#t,
|
|
231
|
+
readOnly,
|
|
232
|
+
onUploadClick: () => this.#triggerFileInput(wrapper),
|
|
233
|
+
onOpenUrlEditor: () => this.#openUrlEditor(wrapper),
|
|
234
|
+
onFileDropped: (file) => { void this.#handleFile(wrapper, file) },
|
|
235
|
+
customActions: this.#config.actions || [],
|
|
236
|
+
runCustomAction: async (handler) => this.#runCustomAction(wrapper, handler),
|
|
237
|
+
})
|
|
238
|
+
if (!readOnly) preloadSourceEditor(wrapper, state.abortController.signal, ['url'])
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
242
|
+
#renderFilled(wrapper) {
|
|
243
|
+
const state = this.#states.get(wrapper)
|
|
244
|
+
if (!state) return
|
|
245
|
+
const readOnly = Boolean(this.#contexts.get(wrapper)?.readOnly)
|
|
246
|
+
renderFilledView(wrapper, state, {
|
|
247
|
+
t: this.#t,
|
|
248
|
+
readOnly,
|
|
249
|
+
mutate: (operation) => this.#mutate(wrapper, operation),
|
|
250
|
+
customActions: this.#config.actions || [],
|
|
251
|
+
onTriggerFileInput: () => this.#triggerFileInput(wrapper),
|
|
252
|
+
onOpenUrlEditor: () => this.#openUrlEditor(wrapper),
|
|
253
|
+
onDelete: () => this.#deleteImage(wrapper),
|
|
254
|
+
runCustomAction: async (handler) => this.#runCustomAction(wrapper, handler),
|
|
255
|
+
})
|
|
256
|
+
if (!readOnly) preloadSourceEditor(wrapper, state.abortController.signal, ['url'])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
260
|
+
#triggerFileInput(wrapper) {
|
|
261
|
+
if (this.#contexts.get(wrapper)?.readOnly) return
|
|
262
|
+
triggerFileInput({
|
|
263
|
+
accept: 'image/*',
|
|
264
|
+
signal: this.#states.get(wrapper)?.abortController?.signal,
|
|
265
|
+
onFiles: (files) => {
|
|
266
|
+
if (files[0]) void this.#handleFile(wrapper, files[0])
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {HTMLElement} wrapper
|
|
273
|
+
* @param {File} file
|
|
274
|
+
* @returns {Promise<void>}
|
|
275
|
+
*/
|
|
276
|
+
async #handleFile(wrapper, file) {
|
|
277
|
+
const state = this.#states.get(wrapper)
|
|
278
|
+
const context = this.#contexts.get(wrapper)
|
|
279
|
+
if (!state || !context || context.readOnly || !isSupportedImageFile(file)) return
|
|
280
|
+
const controller = state.beginTask()
|
|
281
|
+
wrapper.classList.add(CSS.loading)
|
|
282
|
+
try {
|
|
283
|
+
await this.#uploader.handle(file, (result) => {
|
|
284
|
+
if (controller.signal.aborted || this.#states.get(wrapper) !== state) return
|
|
285
|
+
this.#mutate(wrapper, () => {
|
|
286
|
+
state.data.file = { url: result.url }
|
|
287
|
+
if (result.alt && !state.data.caption) state.data.caption = result.alt
|
|
288
|
+
this.#renderFilled(wrapper)
|
|
289
|
+
})
|
|
290
|
+
}, controller.signal)
|
|
291
|
+
} finally {
|
|
292
|
+
if (state.finishTask(controller)) wrapper.classList.remove(CSS.loading)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {HTMLElement} wrapper
|
|
298
|
+
* @param {(context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null>} handler
|
|
299
|
+
* @returns {Promise<void>}
|
|
300
|
+
*/
|
|
301
|
+
async #runCustomAction(wrapper, handler) {
|
|
302
|
+
const state = this.#states.get(wrapper)
|
|
303
|
+
if (!state || this.#contexts.get(wrapper)?.readOnly) return
|
|
304
|
+
const controller = state.beginTask()
|
|
305
|
+
wrapper.classList.add(CSS.loading)
|
|
306
|
+
try {
|
|
307
|
+
const result = await handler({ signal: controller.signal })
|
|
308
|
+
const url = sanitizeMediaUrl(result?.url || '')
|
|
309
|
+
if (!controller.signal.aborted && this.#states.get(wrapper) === state && url) {
|
|
310
|
+
this.#mutate(wrapper, () => {
|
|
311
|
+
state.data.file = { url }
|
|
312
|
+
if (typeof result?.alt === 'string' && result.alt && !state.data.caption) {
|
|
313
|
+
state.data.caption = result.alt
|
|
314
|
+
}
|
|
315
|
+
this.#renderFilled(wrapper)
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
} catch {
|
|
319
|
+
// Action was canceled or failed.
|
|
320
|
+
} finally {
|
|
321
|
+
if (state.finishTask(controller)) wrapper.classList.remove(CSS.loading)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
326
|
+
#openUrlEditor(wrapper) {
|
|
327
|
+
const state = this.#states.get(wrapper)
|
|
328
|
+
if (!state || this.#contexts.get(wrapper)?.readOnly) return
|
|
329
|
+
openSourceEditor({
|
|
330
|
+
wrapper,
|
|
331
|
+
signal: state.abortController.signal,
|
|
332
|
+
kind: 'url',
|
|
333
|
+
title: this.#t('urlEditorTitle', 'Insert image by URL'),
|
|
334
|
+
label: this.#t('urlEditorLabel', 'Image URL'),
|
|
335
|
+
placeholder: this.#t('urlEditorPlaceholder', 'https://example.com/image.jpg'),
|
|
336
|
+
submitText: this.#t('sourceSubmit', 'Insert'),
|
|
337
|
+
cancelText: this.#t('sourceCancel', 'Cancel'),
|
|
338
|
+
invalidText: this.#t('invalidUrl', 'Enter a valid image URL.'),
|
|
339
|
+
normalize: sanitizeMediaUrl,
|
|
340
|
+
onSubmit: (url) => {
|
|
341
|
+
const current = this.#states.get(wrapper)
|
|
342
|
+
if (current !== state || this.#contexts.get(wrapper)?.readOnly) return
|
|
343
|
+
state.cancelTask()
|
|
344
|
+
wrapper.classList.remove(CSS.loading)
|
|
345
|
+
this.#mutate(wrapper, () => {
|
|
346
|
+
state.data.file = { url }
|
|
347
|
+
this.#renderFilled(wrapper)
|
|
348
|
+
})
|
|
349
|
+
},
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** @param {HTMLElement} wrapper @returns {void} */
|
|
354
|
+
#deleteImage(wrapper) {
|
|
355
|
+
const state = this.#states.get(wrapper)
|
|
356
|
+
if (!state) return
|
|
357
|
+
state.cancelTask()
|
|
358
|
+
wrapper.classList.remove(CSS.loading)
|
|
359
|
+
this.#mutate(wrapper, () => {
|
|
360
|
+
state.data.file = { url: '' }
|
|
361
|
+
state.data.styles = {}
|
|
362
|
+
this.#renderEmpty(wrapper)
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|