@shelamkoff/rector 1.0.1 → 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 -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/gallery.css +6 -1
- 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/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/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/package.json +32 -32
|
@@ -6,6 +6,14 @@ declare const _default: {
|
|
|
6
6
|
'plugin.image.dropzone': string;
|
|
7
7
|
'plugin.image.dropzoneUpload': string;
|
|
8
8
|
'plugin.image.dropzoneText': string;
|
|
9
|
+
'plugin.image.dropzoneUrlPrefix': string;
|
|
10
|
+
'plugin.image.dropzoneUrl': string;
|
|
11
|
+
'plugin.image.urlEditorTitle': string;
|
|
12
|
+
'plugin.image.urlEditorLabel': string;
|
|
13
|
+
'plugin.image.urlEditorPlaceholder': string;
|
|
14
|
+
'plugin.image.sourceSubmit': string;
|
|
15
|
+
'plugin.image.sourceCancel': string;
|
|
16
|
+
'plugin.image.invalidUrl': string;
|
|
9
17
|
'plugin.image.replace': string;
|
|
10
18
|
'plugin.image.delete': string;
|
|
11
19
|
'plugin.image.caption': string;
|
|
@@ -6,6 +6,14 @@ export default {
|
|
|
6
6
|
'plugin.image.dropzone': 'or drag and drop a file here',
|
|
7
7
|
'plugin.image.dropzoneUpload': 'Upload',
|
|
8
8
|
'plugin.image.dropzoneText': 'an image from your device or drag and drop it here',
|
|
9
|
+
'plugin.image.dropzoneUrlPrefix': 'or',
|
|
10
|
+
'plugin.image.dropzoneUrl': 'insert by URL',
|
|
11
|
+
'plugin.image.urlEditorTitle': 'Insert image by URL',
|
|
12
|
+
'plugin.image.urlEditorLabel': 'Image URL',
|
|
13
|
+
'plugin.image.urlEditorPlaceholder': 'https://example.com/image.jpg',
|
|
14
|
+
'plugin.image.sourceSubmit': 'Insert',
|
|
15
|
+
'plugin.image.sourceCancel': 'Cancel',
|
|
16
|
+
'plugin.image.invalidUrl': 'Enter a valid image URL.',
|
|
9
17
|
'plugin.image.replace': 'Replace',
|
|
10
18
|
'plugin.image.delete': 'Delete',
|
|
11
19
|
'plugin.image.caption': 'Caption',
|
|
@@ -33,6 +41,6 @@ export default {
|
|
|
33
41
|
'plugin.image.value.fill': 'Fill',
|
|
34
42
|
'plugin.image.value.scale-down': 'Scale down',
|
|
35
43
|
'plugin.image.value.solid': 'Solid',
|
|
36
|
-
'plugin.image.value.dashed': 'Dashed',
|
|
37
|
-
'plugin.image.emptyReadonly': 'No image',
|
|
38
|
-
}
|
|
44
|
+
'plugin.image.value.dashed': 'Dashed',
|
|
45
|
+
'plugin.image.emptyReadonly': 'No image',
|
|
46
|
+
}
|
|
@@ -6,6 +6,14 @@ declare const _default: {
|
|
|
6
6
|
'plugin.image.dropzone': string;
|
|
7
7
|
'plugin.image.dropzoneUpload': string;
|
|
8
8
|
'plugin.image.dropzoneText': string;
|
|
9
|
+
'plugin.image.dropzoneUrlPrefix': string;
|
|
10
|
+
'plugin.image.dropzoneUrl': string;
|
|
11
|
+
'plugin.image.urlEditorTitle': string;
|
|
12
|
+
'plugin.image.urlEditorLabel': string;
|
|
13
|
+
'plugin.image.urlEditorPlaceholder': string;
|
|
14
|
+
'plugin.image.sourceSubmit': string;
|
|
15
|
+
'plugin.image.sourceCancel': string;
|
|
16
|
+
'plugin.image.invalidUrl': string;
|
|
9
17
|
'plugin.image.replace': string;
|
|
10
18
|
'plugin.image.delete': string;
|
|
11
19
|
'plugin.image.caption': string;
|
|
@@ -6,6 +6,14 @@ export default {
|
|
|
6
6
|
'plugin.image.dropzone': 'или перетащите файл сюда',
|
|
7
7
|
'plugin.image.dropzoneUpload': 'Загрузите',
|
|
8
8
|
'plugin.image.dropzoneText': 'изображение с устройства или перетащите его сюда',
|
|
9
|
+
'plugin.image.dropzoneUrlPrefix': 'или',
|
|
10
|
+
'plugin.image.dropzoneUrl': 'вставьте по URL',
|
|
11
|
+
'plugin.image.urlEditorTitle': 'Вставка изображения по URL',
|
|
12
|
+
'plugin.image.urlEditorLabel': 'URL изображения',
|
|
13
|
+
'plugin.image.urlEditorPlaceholder': 'https://example.com/image.jpg',
|
|
14
|
+
'plugin.image.sourceSubmit': 'Вставить',
|
|
15
|
+
'plugin.image.sourceCancel': 'Отмена',
|
|
16
|
+
'plugin.image.invalidUrl': 'Введите корректный URL изображения.',
|
|
9
17
|
'plugin.image.replace': 'Заменить',
|
|
10
18
|
'plugin.image.delete': 'Удалить',
|
|
11
19
|
'plugin.image.caption': 'Подпись',
|
|
@@ -33,6 +41,6 @@ export default {
|
|
|
33
41
|
'plugin.image.value.fill': 'Растянуть',
|
|
34
42
|
'plugin.image.value.scale-down': 'Только уменьшать',
|
|
35
43
|
'plugin.image.value.solid': 'Сплошная',
|
|
36
|
-
'plugin.image.value.dashed': 'Штриховая',
|
|
37
|
-
'plugin.image.emptyReadonly': 'Нет изображения',
|
|
38
|
-
}
|
|
44
|
+
'plugin.image.value.dashed': 'Штриховая',
|
|
45
|
+
'plugin.image.emptyReadonly': 'Нет изображения',
|
|
46
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @property {boolean} readOnly
|
|
5
5
|
* @property {(file: File) => void} onFileDropped
|
|
6
6
|
* @property {() => void} onUploadClick
|
|
7
|
+
* @property {() => void} onOpenUrlEditor
|
|
7
8
|
* @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null> }>} customActions
|
|
8
9
|
* @property {(handler: (context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null>) => Promise<void>} runCustomAction
|
|
9
10
|
*/
|
|
@@ -25,6 +26,7 @@ export type EmptyViewDeps = {
|
|
|
25
26
|
readOnly: boolean;
|
|
26
27
|
onFileDropped: (file: File) => void;
|
|
27
28
|
onUploadClick: () => void;
|
|
29
|
+
onOpenUrlEditor: () => void;
|
|
28
30
|
customActions: Array<{
|
|
29
31
|
icon?: string;
|
|
30
32
|
label: string;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { renderDropzone } from '../shared/dropzone.js'
|
|
2
|
-
import { isSupportedImageFile } from '../shared/fileInput.js'
|
|
1
|
+
import { renderDropzone } from '../shared/dropzone.js'
|
|
2
|
+
import { isSupportedImageFile } from '../shared/fileInput.js'
|
|
3
3
|
import { CSS } from './css.js'
|
|
4
4
|
import { ICON_SELECT } from './icons.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @typedef {Object} EmptyViewDeps
|
|
8
|
-
* @property {(key: string, fallback: string) => string} t
|
|
9
|
-
* @property {boolean} readOnly
|
|
7
|
+
* @typedef {Object} EmptyViewDeps
|
|
8
|
+
* @property {(key: string, fallback: string) => string} t
|
|
9
|
+
* @property {boolean} readOnly
|
|
10
10
|
* @property {(file: File) => void} onFileDropped
|
|
11
11
|
* @property {() => void} onUploadClick
|
|
12
|
+
* @property {() => void} onOpenUrlEditor
|
|
12
13
|
* @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null> }>} customActions
|
|
13
14
|
* @property {(handler: (context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null>) => Promise<void>} runCustomAction
|
|
14
15
|
*/
|
|
@@ -20,11 +21,11 @@ import { ICON_SELECT } from './icons.js'
|
|
|
20
21
|
* Listeners are attached with the state's AbortSignal so they're
|
|
21
22
|
* automatically removed on the next render or on disposal.
|
|
22
23
|
*
|
|
23
|
-
* @param {HTMLElement} wrapper
|
|
24
|
-
* @param {import('./state.js').ImageState} state
|
|
25
|
-
* @param {EmptyViewDeps} deps
|
|
26
|
-
* @returns {void}
|
|
27
|
-
*/
|
|
24
|
+
* @param {HTMLElement} wrapper
|
|
25
|
+
* @param {import('./state.js').ImageState} state
|
|
26
|
+
* @param {EmptyViewDeps} deps
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
28
29
|
export function renderEmptyView(wrapper, state, deps) {
|
|
29
30
|
state.resetTransient()
|
|
30
31
|
|
|
@@ -41,10 +42,15 @@ export function renderEmptyView(wrapper, state, deps) {
|
|
|
41
42
|
filled: CSS.filled,
|
|
42
43
|
}, {
|
|
43
44
|
iconHtml: ICON_SELECT,
|
|
44
|
-
uploadText: deps.t('dropzoneUpload', 'Upload'),
|
|
45
|
-
afterText: deps.t('dropzoneText', 'an image from your device or drag and drop it here'),
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
uploadText: deps.t('dropzoneUpload', 'Upload'),
|
|
46
|
+
afterText: deps.t('dropzoneText', 'an image from your device or drag and drop it here'),
|
|
47
|
+
inlineActions: [{
|
|
48
|
+
prefix: deps.t('dropzoneUrlPrefix', 'or'),
|
|
49
|
+
label: deps.t('dropzoneUrl', 'insert by URL'),
|
|
50
|
+
onSelect: deps.onOpenUrlEditor,
|
|
51
|
+
}],
|
|
52
|
+
readOnly: deps.readOnly,
|
|
53
|
+
emptyText: deps.t('emptyReadonly', 'No image'),
|
|
48
54
|
onUploadClick: deps.onUploadClick,
|
|
49
55
|
actions: deps.customActions.map(action => ({
|
|
50
56
|
icon: action.icon,
|
|
@@ -53,7 +59,7 @@ export function renderEmptyView(wrapper, state, deps) {
|
|
|
53
59
|
})),
|
|
54
60
|
onDrop: (dt) => {
|
|
55
61
|
const file = dt.files[0]
|
|
56
|
-
if (file && isSupportedImageFile(file)) {
|
|
62
|
+
if (file && isSupportedImageFile(file)) {
|
|
57
63
|
deps.onFileDropped(file)
|
|
58
64
|
}
|
|
59
65
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @property {(key: string, fallback: string) => string} t
|
|
4
4
|
* @property {boolean} readOnly
|
|
5
5
|
* @property {() => void} onTriggerFileInput
|
|
6
|
-
* @property {() => void}
|
|
6
|
+
* @property {() => void} onOpenUrlEditor
|
|
7
7
|
* @property {() => void} onDelete
|
|
8
8
|
* @property {(operation: () => void) => void} mutate
|
|
9
9
|
* @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null> }>} customActions
|
|
@@ -27,7 +27,7 @@ export type FilledViewDeps = {
|
|
|
27
27
|
t: (key: string, fallback: string) => string;
|
|
28
28
|
readOnly: boolean;
|
|
29
29
|
onTriggerFileInput: () => void;
|
|
30
|
-
|
|
30
|
+
onOpenUrlEditor: () => void;
|
|
31
31
|
onDelete: () => void;
|
|
32
32
|
mutate: (operation: () => void) => void;
|
|
33
33
|
customActions: Array<{
|
|
@@ -8,13 +8,14 @@ import {
|
|
|
8
8
|
} from './icons.js'
|
|
9
9
|
import { applyInlineStyles } from './styles.js'
|
|
10
10
|
import { buildSettingsPanel } from './settings.js'
|
|
11
|
+
import { createPluginLayer } from '../shared/layer.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
* @typedef {Object} FilledViewDeps
|
|
14
|
-
* @property {(key: string, fallback: string) => string} t
|
|
15
|
-
* @property {boolean} readOnly
|
|
14
|
+
* @typedef {Object} FilledViewDeps
|
|
15
|
+
* @property {(key: string, fallback: string) => string} t
|
|
16
|
+
* @property {boolean} readOnly
|
|
16
17
|
* @property {() => void} onTriggerFileInput
|
|
17
|
-
* @property {() => void}
|
|
18
|
+
* @property {() => void} onOpenUrlEditor
|
|
18
19
|
* @property {() => void} onDelete
|
|
19
20
|
* @property {(operation: () => void) => void} mutate
|
|
20
21
|
* @property {Array<{ icon?: string, label: string, handler: (context: { signal: AbortSignal }) => Promise<{url: string, alt?: string} | null> }>} customActions
|
|
@@ -29,11 +30,11 @@ import { buildSettingsPanel } from './settings.js'
|
|
|
29
30
|
* Listeners use the state's AbortSignal — automatically removed on the next
|
|
30
31
|
* render or on disposal.
|
|
31
32
|
*
|
|
32
|
-
* @param {HTMLElement} wrapper
|
|
33
|
-
* @param {import('./state.js').ImageState} state
|
|
34
|
-
* @param {FilledViewDeps} deps
|
|
35
|
-
* @returns {void}
|
|
36
|
-
*/
|
|
33
|
+
* @param {HTMLElement} wrapper
|
|
34
|
+
* @param {import('./state.js').ImageState} state
|
|
35
|
+
* @param {FilledViewDeps} deps
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
37
38
|
export function renderFilledView(wrapper, state, deps) {
|
|
38
39
|
state.resetTransient()
|
|
39
40
|
wrapper.innerHTML = ''
|
|
@@ -45,24 +46,24 @@ export function renderFilledView(wrapper, state, deps) {
|
|
|
45
46
|
container.className = CSS.imageContainer
|
|
46
47
|
|
|
47
48
|
const img = document.createElement('img')
|
|
48
|
-
img.className = CSS.image
|
|
49
|
-
setSafeUrlAttribute(img, 'src', state.data.file.url, 'media')
|
|
50
|
-
applyInlineStyles(state, img, container)
|
|
49
|
+
img.className = CSS.image
|
|
50
|
+
setSafeUrlAttribute(img, 'src', state.data.file.url, 'media')
|
|
51
|
+
applyInlineStyles(state, img, container)
|
|
51
52
|
container.appendChild(img)
|
|
52
53
|
|
|
53
|
-
// Caption
|
|
54
|
-
const caption = renderCaption(state, signal, deps)
|
|
55
|
-
img.alt = caption.textContent?.trim() || ''
|
|
56
|
-
if (!deps.readOnly) {
|
|
57
|
-
caption.addEventListener('input', () => {
|
|
58
|
-
img.alt = caption.textContent?.trim() || ''
|
|
59
|
-
}, { signal })
|
|
60
|
-
}
|
|
54
|
+
// Caption
|
|
55
|
+
const caption = renderCaption(state, signal, deps)
|
|
56
|
+
img.alt = caption.textContent?.trim() || ''
|
|
57
|
+
if (!deps.readOnly) {
|
|
58
|
+
caption.addEventListener('input', () => {
|
|
59
|
+
img.alt = caption.textContent?.trim() || ''
|
|
60
|
+
}, { signal })
|
|
61
|
+
}
|
|
61
62
|
container.appendChild(caption)
|
|
62
63
|
wrapper.appendChild(container)
|
|
63
64
|
|
|
64
65
|
// Action bar
|
|
65
|
-
if (!deps.readOnly) wrapper.appendChild(renderActions(wrapper, state, deps, signal))
|
|
66
|
+
if (!deps.readOnly) wrapper.appendChild(renderActions(wrapper, state, deps, signal))
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/**
|
|
@@ -74,7 +75,7 @@ export function renderFilledView(wrapper, state, deps) {
|
|
|
74
75
|
function renderCaption(state, signal, deps) {
|
|
75
76
|
const caption = document.createElement('div')
|
|
76
77
|
caption.className = CSS.caption
|
|
77
|
-
caption.contentEditable = deps.readOnly ? 'false' : 'true'
|
|
78
|
+
caption.contentEditable = deps.readOnly ? 'false' : 'true'
|
|
78
79
|
caption.dataset.placeholder = deps.t('caption', 'Caption')
|
|
79
80
|
|
|
80
81
|
if (state.data.caption) {
|
|
@@ -96,23 +97,23 @@ function renderCaption(state, signal, deps) {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
if (!deps.readOnly) {
|
|
100
|
-
caption.addEventListener('input', syncEmpty, { signal })
|
|
101
|
-
caption.addEventListener('focus', () => caption.removeAttribute('data-empty'), { signal })
|
|
102
|
-
caption.addEventListener('blur', () => {
|
|
103
|
-
if (!caption.textContent?.trim()) {
|
|
104
|
-
caption.innerHTML = ''
|
|
105
|
-
caption.setAttribute('data-empty', 'true')
|
|
106
|
-
state.data.caption = ''
|
|
107
|
-
}
|
|
108
|
-
}, { signal })
|
|
109
|
-
caption.addEventListener('keydown', (e) => {
|
|
110
|
-
if (e.key === 'Backspace' && !caption.textContent?.trim()) {
|
|
111
|
-
e.preventDefault()
|
|
112
|
-
e.stopPropagation()
|
|
113
|
-
}
|
|
114
|
-
}, { signal })
|
|
115
|
-
}
|
|
100
|
+
if (!deps.readOnly) {
|
|
101
|
+
caption.addEventListener('input', syncEmpty, { signal })
|
|
102
|
+
caption.addEventListener('focus', () => caption.removeAttribute('data-empty'), { signal })
|
|
103
|
+
caption.addEventListener('blur', () => {
|
|
104
|
+
if (!caption.textContent?.trim()) {
|
|
105
|
+
caption.innerHTML = ''
|
|
106
|
+
caption.setAttribute('data-empty', 'true')
|
|
107
|
+
state.data.caption = ''
|
|
108
|
+
}
|
|
109
|
+
}, { signal })
|
|
110
|
+
caption.addEventListener('keydown', (e) => {
|
|
111
|
+
if (e.key === 'Backspace' && !caption.textContent?.trim()) {
|
|
112
|
+
e.preventDefault()
|
|
113
|
+
e.stopPropagation()
|
|
114
|
+
}
|
|
115
|
+
}, { signal })
|
|
116
|
+
}
|
|
116
117
|
|
|
117
118
|
return caption
|
|
118
119
|
}
|
|
@@ -135,27 +136,33 @@ function renderActions(wrapper, state, deps, signal) {
|
|
|
135
136
|
dropdown.className = CSS.dropdown
|
|
136
137
|
|
|
137
138
|
const settingsBtn = document.createElement('button')
|
|
138
|
-
settingsBtn.type = 'button'
|
|
139
|
-
settingsBtn.className = CSS.actionBtn
|
|
140
|
-
settingsBtn.innerHTML = `${ICON_SETTINGS} ${deps.t('settings', 'Settings')}`
|
|
141
|
-
settingsBtn.setAttribute('aria-haspopup', 'true')
|
|
142
|
-
settingsBtn.setAttribute('aria-expanded', 'false')
|
|
143
|
-
|
|
144
|
-
const panel = buildSettingsPanel(wrapper, state, settingsDeps)
|
|
145
|
-
panel.setAttribute('role', 'group')
|
|
146
|
-
panel.setAttribute('aria-label', deps.t('settings', 'Settings'))
|
|
139
|
+
settingsBtn.type = 'button'
|
|
140
|
+
settingsBtn.className = CSS.actionBtn
|
|
141
|
+
settingsBtn.innerHTML = `${ICON_SETTINGS} ${deps.t('settings', 'Settings')}`
|
|
142
|
+
settingsBtn.setAttribute('aria-haspopup', 'true')
|
|
143
|
+
settingsBtn.setAttribute('aria-expanded', 'false')
|
|
144
|
+
|
|
145
|
+
const panel = buildSettingsPanel(wrapper, state, settingsDeps)
|
|
146
|
+
panel.setAttribute('role', 'group')
|
|
147
|
+
panel.setAttribute('aria-label', deps.t('settings', 'Settings'))
|
|
148
|
+
const settingsLayer = createPluginLayer(wrapper, signal)
|
|
149
|
+
const closeSettings = () => {
|
|
150
|
+
dropdown.classList.remove(CSS.dropdownOpen)
|
|
151
|
+
settingsBtn.setAttribute('aria-expanded', 'false')
|
|
152
|
+
settingsLayer.close()
|
|
153
|
+
}
|
|
147
154
|
|
|
148
155
|
settingsBtn.addEventListener('click', (e) => {
|
|
149
156
|
e.stopPropagation()
|
|
150
157
|
const isOpen = dropdown.classList.contains(CSS.dropdownOpen)
|
|
151
|
-
if (isOpen) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
if (isOpen) {
|
|
159
|
+
closeSettings()
|
|
160
|
+
} else {
|
|
161
|
+
settingsLayer.open()
|
|
155
162
|
panel.style.top = ''
|
|
156
163
|
panel.style.bottom = ''
|
|
157
|
-
dropdown.classList.add(CSS.dropdownOpen)
|
|
158
|
-
settingsBtn.setAttribute('aria-expanded', 'true')
|
|
164
|
+
dropdown.classList.add(CSS.dropdownOpen)
|
|
165
|
+
settingsBtn.setAttribute('aria-expanded', 'true')
|
|
159
166
|
|
|
160
167
|
const btnRect = settingsBtn.getBoundingClientRect()
|
|
161
168
|
const panelRect = panel.getBoundingClientRect()
|
|
@@ -173,19 +180,17 @@ function renderActions(wrapper, state, deps, signal) {
|
|
|
173
180
|
dropdown.append(settingsBtn, panel)
|
|
174
181
|
|
|
175
182
|
document.addEventListener('click', (e) => {
|
|
176
|
-
if (!dropdown.contains(/** @type {Node} */ (e.target))) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
settingsBtn.focus()
|
|
188
|
-
}, { signal })
|
|
183
|
+
if (!dropdown.contains(/** @type {Node} */ (e.target))) {
|
|
184
|
+
closeSettings()
|
|
185
|
+
}
|
|
186
|
+
}, { signal })
|
|
187
|
+
|
|
188
|
+
dropdown.addEventListener('keydown', (event) => {
|
|
189
|
+
if (event.key !== 'Escape' || !dropdown.classList.contains(CSS.dropdownOpen)) return
|
|
190
|
+
event.preventDefault()
|
|
191
|
+
closeSettings()
|
|
192
|
+
settingsBtn.focus()
|
|
193
|
+
}, { signal })
|
|
189
194
|
|
|
190
195
|
const sep1 = makeSep()
|
|
191
196
|
|
|
@@ -209,10 +214,10 @@ function renderActions(wrapper, state, deps, signal) {
|
|
|
209
214
|
|
|
210
215
|
// Delete (icon only)
|
|
211
216
|
const deleteBtn = document.createElement('button')
|
|
212
|
-
deleteBtn.type = 'button'
|
|
213
|
-
deleteBtn.className = `${CSS.actionBtn} ${CSS.actionBtnDanger}`
|
|
214
|
-
deleteBtn.innerHTML = ICON_TRASH
|
|
215
|
-
deleteBtn.setAttribute('aria-label', deps.t('delete', 'Delete'))
|
|
217
|
+
deleteBtn.type = 'button'
|
|
218
|
+
deleteBtn.className = `${CSS.actionBtn} ${CSS.actionBtnDanger}`
|
|
219
|
+
deleteBtn.innerHTML = ICON_TRASH
|
|
220
|
+
deleteBtn.setAttribute('aria-label', deps.t('delete', 'Delete'))
|
|
216
221
|
deleteBtn.addEventListener('mousedown', (e) => e.preventDefault(), { signal })
|
|
217
222
|
deleteBtn.addEventListener('click', (e) => {
|
|
218
223
|
e.stopPropagation()
|
|
@@ -264,7 +269,7 @@ function showReplaceView(actions, mainView, deps, signal) {
|
|
|
264
269
|
|
|
265
270
|
view.appendChild(makeActionBtn(
|
|
266
271
|
`${ICON_URL} URL`,
|
|
267
|
-
() => { deps.
|
|
272
|
+
() => { deps.onOpenUrlEditor(); restore() },
|
|
268
273
|
signal,
|
|
269
274
|
))
|
|
270
275
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @property {string} afterText Localized text after the upload link.
|
|
21
21
|
* @property {() => void} onUploadClick Opens the file input.
|
|
22
22
|
* @property {(dataTransfer: DataTransfer) => void} onDrop Handles dropped files.
|
|
23
|
+
* @property {Array<{ label: string, prefix?: string, onSelect: () => void }>} [inlineActions]
|
|
23
24
|
* @property {Array<{ icon?: string, label: string, onSelect: () => void }>} [actions]
|
|
24
25
|
* @property {boolean} [readOnly] Render a non-interactive empty state.
|
|
25
26
|
* @property {string} [emptyText] Text shown for an empty read-only block.
|
|
@@ -90,6 +91,11 @@ export type DropzoneConfig = {
|
|
|
90
91
|
* Handles dropped files.
|
|
91
92
|
*/
|
|
92
93
|
onDrop: (dataTransfer: DataTransfer) => void;
|
|
94
|
+
inlineActions?: Array<{
|
|
95
|
+
label: string;
|
|
96
|
+
prefix?: string;
|
|
97
|
+
onSelect: () => void;
|
|
98
|
+
}>;
|
|
93
99
|
actions?: Array<{
|
|
94
100
|
icon?: string;
|
|
95
101
|
label: string;
|
|
@@ -5,38 +5,39 @@
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {Object} DropzoneCssMap
|
|
8
|
-
* @property {string} select Wrapper class.
|
|
9
|
-
* @property {string} selectIcon Icon container class.
|
|
10
|
-
* @property {string} selectText Text container class.
|
|
11
|
-
* @property {string} selectLink Upload link class.
|
|
12
|
-
* @property {string} dropzoneActive Class added during dragover.
|
|
13
|
-
* @property {string} filled Class removed from the wrapper during rendering.
|
|
14
|
-
* @property {string} [selectActions] Optional application-source list class.
|
|
15
|
-
* @property {string} [selectAction] Optional application-source button class.
|
|
8
|
+
* @property {string} select Wrapper class.
|
|
9
|
+
* @property {string} selectIcon Icon container class.
|
|
10
|
+
* @property {string} selectText Text container class.
|
|
11
|
+
* @property {string} selectLink Upload link class.
|
|
12
|
+
* @property {string} dropzoneActive Class added during dragover.
|
|
13
|
+
* @property {string} filled Class removed from the wrapper during rendering.
|
|
14
|
+
* @property {string} [selectActions] Optional application-source list class.
|
|
15
|
+
* @property {string} [selectAction] Optional application-source button class.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @typedef {Object} DropzoneConfig
|
|
20
|
-
* @property {string} iconHtml SVG icon markup.
|
|
21
|
-
* @property {string} uploadText Localized upload-link text.
|
|
22
|
-
* @property {string} afterText Localized text after the upload link.
|
|
23
|
-
* @property {() => void} onUploadClick Opens the file input.
|
|
24
|
-
* @property {(dataTransfer: DataTransfer) => void} onDrop Handles dropped files.
|
|
25
|
-
* @property {Array<{
|
|
26
|
-
* @property {
|
|
27
|
-
* @property {
|
|
20
|
+
* @property {string} iconHtml SVG icon markup.
|
|
21
|
+
* @property {string} uploadText Localized upload-link text.
|
|
22
|
+
* @property {string} afterText Localized text after the upload link.
|
|
23
|
+
* @property {() => void} onUploadClick Opens the file input.
|
|
24
|
+
* @property {(dataTransfer: DataTransfer) => void} onDrop Handles dropped files.
|
|
25
|
+
* @property {Array<{ label: string, prefix?: string, onSelect: () => void }>} [inlineActions]
|
|
26
|
+
* @property {Array<{ icon?: string, label: string, onSelect: () => void }>} [actions]
|
|
27
|
+
* @property {boolean} [readOnly] Render a non-interactive empty state.
|
|
28
|
+
* @property {string} [emptyText] Text shown for an empty read-only block.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Render a dropzone (empty-state) view into a wrapper element.
|
|
32
33
|
* Clears the wrapper, removes the `filled` class, and wires drag-and-drop.
|
|
33
34
|
*
|
|
34
|
-
* @param {HTMLElement} wrapper Block wrapper element.
|
|
35
|
-
* @param {AbortSignal} signal Enables automatic listener cleanup.
|
|
36
|
-
* @param {DropzoneCssMap} css — plugin-specific CSS class names
|
|
37
|
-
* @param {DropzoneConfig} config — icon, text, and callbacks
|
|
38
|
-
* @returns {void}
|
|
39
|
-
*/
|
|
35
|
+
* @param {HTMLElement} wrapper Block wrapper element.
|
|
36
|
+
* @param {AbortSignal} signal Enables automatic listener cleanup.
|
|
37
|
+
* @param {DropzoneCssMap} css — plugin-specific CSS class names
|
|
38
|
+
* @param {DropzoneConfig} config — icon, text, and callbacks
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
40
41
|
export function renderDropzone(wrapper, signal, css, config) {
|
|
41
42
|
wrapper.innerHTML = ''
|
|
42
43
|
wrapper.classList.remove(css.filled)
|
|
@@ -48,17 +49,17 @@ export function renderDropzone(wrapper, signal, css, config) {
|
|
|
48
49
|
icon.className = css.selectIcon
|
|
49
50
|
icon.innerHTML = config.iconHtml
|
|
50
51
|
|
|
51
|
-
const text = document.createElement('div')
|
|
52
|
-
text.className = css.selectText
|
|
53
|
-
|
|
54
|
-
if (config.readOnly) {
|
|
55
|
-
text.textContent = config.emptyText || config.afterText
|
|
56
|
-
select.append(icon, text)
|
|
57
|
-
wrapper.appendChild(select)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const uploadLink = document.createElement('button')
|
|
52
|
+
const text = document.createElement('div')
|
|
53
|
+
text.className = css.selectText
|
|
54
|
+
|
|
55
|
+
if (config.readOnly) {
|
|
56
|
+
text.textContent = config.emptyText || config.afterText
|
|
57
|
+
select.append(icon, text)
|
|
58
|
+
wrapper.appendChild(select)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const uploadLink = document.createElement('button')
|
|
62
63
|
uploadLink.type = 'button'
|
|
63
64
|
uploadLink.className = css.selectLink
|
|
64
65
|
uploadLink.textContent = config.uploadText
|
|
@@ -67,9 +68,19 @@ export function renderDropzone(wrapper, signal, css, config) {
|
|
|
67
68
|
config.onUploadClick()
|
|
68
69
|
}, { signal })
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
text.append(uploadLink, document.createTextNode(' ' + config.afterText))
|
|
72
|
+
for (const action of config.inlineActions || []) {
|
|
73
|
+
if (action.prefix) text.append(document.createTextNode(' ' + action.prefix + ' '))
|
|
74
|
+
const button = document.createElement('button')
|
|
75
|
+
button.type = 'button'
|
|
76
|
+
button.className = css.selectLink
|
|
77
|
+
button.textContent = action.label
|
|
78
|
+
button.addEventListener('click', (event) => {
|
|
79
|
+
event.stopPropagation()
|
|
80
|
+
action.onSelect()
|
|
81
|
+
}, { signal })
|
|
82
|
+
text.appendChild(button)
|
|
83
|
+
}
|
|
73
84
|
select.append(icon, text)
|
|
74
85
|
|
|
75
86
|
if (css.selectActions && css.selectAction && config.actions?.length) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep a plugin surface above neighbouring editor blocks while it is open.
|
|
3
|
+
* Calls are reference-counted so a source editor can coexist with a settings
|
|
4
|
+
* panel owned by the same block.
|
|
5
|
+
*
|
|
6
|
+
* @param {HTMLElement} owner Element inside the plugin block.
|
|
7
|
+
* @param {AbortSignal} signal Plugin render lifecycle signal.
|
|
8
|
+
* @returns {{ open: () => void, close: () => void }}
|
|
9
|
+
*/
|
|
10
|
+
export function createPluginLayer(owner: HTMLElement, signal: AbortSignal): {
|
|
11
|
+
open: () => void;
|
|
12
|
+
close: () => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const openLayers = new WeakMap()
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keep a plugin surface above neighbouring editor blocks while it is open.
|
|
5
|
+
* Calls are reference-counted so a source editor can coexist with a settings
|
|
6
|
+
* panel owned by the same block.
|
|
7
|
+
*
|
|
8
|
+
* @param {HTMLElement} owner Element inside the plugin block.
|
|
9
|
+
* @param {AbortSignal} signal Plugin render lifecycle signal.
|
|
10
|
+
* @returns {{ open: () => void, close: () => void }}
|
|
11
|
+
*/
|
|
12
|
+
export function createPluginLayer(owner, signal) {
|
|
13
|
+
/** @type {HTMLElement | null} */
|
|
14
|
+
let block = null
|
|
15
|
+
let opened = false
|
|
16
|
+
|
|
17
|
+
const close = () => {
|
|
18
|
+
if (!opened || !block) return
|
|
19
|
+
const currentBlock = block
|
|
20
|
+
opened = false
|
|
21
|
+
block = null
|
|
22
|
+
const next = Math.max(0, (openLayers.get(currentBlock) || 1) - 1)
|
|
23
|
+
if (next === 0) {
|
|
24
|
+
openLayers.delete(currentBlock)
|
|
25
|
+
currentBlock.removeAttribute('data-oe-layer-open')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
openLayers.set(currentBlock, next)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
signal.addEventListener('abort', close, { once: true })
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
open() {
|
|
35
|
+
if (opened || signal.aborted) return
|
|
36
|
+
const candidate = owner.closest('.oe-block')
|
|
37
|
+
if (!(candidate instanceof HTMLElement)) return
|
|
38
|
+
block = candidate
|
|
39
|
+
opened = true
|
|
40
|
+
openLayers.set(block, (openLayers.get(block) || 0) + 1)
|
|
41
|
+
block.setAttribute('data-oe-layer-open', 'true')
|
|
42
|
+
},
|
|
43
|
+
close,
|
|
44
|
+
}
|
|
45
|
+
}
|