@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.
Files changed (94) hide show
  1. package/README.md +25 -23
  2. package/dist/core/clipboard/Clipboard.d.ts +2 -0
  3. package/dist/core/clipboard/Clipboard.js +52 -24
  4. package/dist/core/index.js +195 -194
  5. package/dist/core/locale/en.d.ts +1 -0
  6. package/dist/core/locale/en.js +1 -0
  7. package/dist/core/locale/ru.d.ts +1 -0
  8. package/dist/core/locale/ru.js +1 -0
  9. package/dist/core/themes/light.css +1 -1
  10. package/dist/core/themes/variables.css +72 -10
  11. package/dist/inline-plugins/mention/index.js +1 -2
  12. package/dist/locale/en.d.ts +41 -0
  13. package/dist/locale/ru.d.ts +41 -0
  14. package/dist/plugins/attaches/attaches.css +16 -14
  15. package/dist/plugins/attaches/index.js +997 -921
  16. package/dist/plugins/attaches/locale/en.d.ts +9 -0
  17. package/dist/plugins/attaches/locale/en.js +11 -2
  18. package/dist/plugins/attaches/locale/ru.d.ts +9 -0
  19. package/dist/plugins/attaches/locale/ru.js +11 -2
  20. package/dist/plugins/carousel/carousel.css +213 -49
  21. package/dist/plugins/carousel/index.js +918 -859
  22. package/dist/plugins/carousel/locale/en.d.ts +15 -0
  23. package/dist/plugins/carousel/locale/en.js +18 -3
  24. package/dist/plugins/carousel/locale/ru.d.ts +15 -0
  25. package/dist/plugins/carousel/locale/ru.js +18 -3
  26. package/dist/plugins/checklist/index.js +1 -2
  27. package/dist/plugins/code/index.js +1 -2
  28. package/dist/plugins/columns/index.js +1 -2
  29. package/dist/plugins/delimiter/index.js +1 -2
  30. package/dist/plugins/embed/index.js +1 -2
  31. package/dist/plugins/gallery/gallery.css +6 -1
  32. package/dist/plugins/gallery/index.js +375 -355
  33. package/dist/plugins/gallery/locale/en.d.ts +8 -0
  34. package/dist/plugins/gallery/locale/en.js +11 -3
  35. package/dist/plugins/gallery/locale/ru.d.ts +8 -0
  36. package/dist/plugins/gallery/locale/ru.js +11 -3
  37. package/dist/plugins/gallery/view-empty.d.ts +2 -0
  38. package/dist/plugins/gallery/view-empty.js +21 -15
  39. package/dist/plugins/gallery/view-filled.d.ts +2 -2
  40. package/dist/plugins/gallery/view-filled.js +61 -56
  41. package/dist/plugins/heading/index.js +1 -2
  42. package/dist/plugins/image/image.css +7 -2
  43. package/dist/plugins/image/index.js +365 -346
  44. package/dist/plugins/image/locale/en.d.ts +8 -0
  45. package/dist/plugins/image/locale/en.js +11 -3
  46. package/dist/plugins/image/locale/ru.d.ts +8 -0
  47. package/dist/plugins/image/locale/ru.js +11 -3
  48. package/dist/plugins/image/view-empty.d.ts +2 -0
  49. package/dist/plugins/image/view-empty.js +21 -15
  50. package/dist/plugins/image/view-filled.d.ts +2 -2
  51. package/dist/plugins/image/view-filled.js +77 -72
  52. package/dist/plugins/link-preview/index.js +1 -2
  53. package/dist/plugins/list/index.js +1 -2
  54. package/dist/plugins/paragraph/index.js +1 -2
  55. package/dist/plugins/person/index.js +1 -2
  56. package/dist/plugins/poll/index.js +1 -2
  57. package/dist/plugins/quote/index.js +1 -2
  58. package/dist/plugins/raw/index.js +1 -2
  59. package/dist/plugins/shared/dropzone.d.ts +6 -0
  60. package/dist/plugins/shared/dropzone.js +47 -36
  61. package/dist/plugins/shared/layer.d.ts +13 -0
  62. package/dist/plugins/shared/layer.js +45 -0
  63. package/dist/plugins/shared/sourceEditor.css +165 -0
  64. package/dist/plugins/shared/sourceEditor.d.ts +97 -0
  65. package/dist/plugins/shared/sourceEditor.js +260 -0
  66. package/dist/plugins/spoiler/index.js +1 -2
  67. package/dist/plugins/table/index.js +1 -2
  68. package/dist/plugins/toggle/index.js +1 -2
  69. package/dist/plugins/warning/index.js +1 -2
  70. package/dist/renderer/index.js +1 -2
  71. package/dist/renderer/renderers/attaches/index.js +1 -2
  72. package/dist/renderer/renderers/carousel/index.js +24 -24
  73. package/dist/renderer/renderers/carousel/styles.css +8 -2
  74. package/dist/renderer/renderers/checklist/index.js +1 -2
  75. package/dist/renderer/renderers/code/index.js +1 -2
  76. package/dist/renderer/renderers/columns/index.js +1 -2
  77. package/dist/renderer/renderers/delimiter/index.js +1 -2
  78. package/dist/renderer/renderers/embed/index.js +1 -2
  79. package/dist/renderer/renderers/gallery/index.js +1 -2
  80. package/dist/renderer/renderers/heading/index.js +1 -2
  81. package/dist/renderer/renderers/image/index.js +1 -2
  82. package/dist/renderer/renderers/link-preview/index.js +1 -2
  83. package/dist/renderer/renderers/list/index.js +1 -2
  84. package/dist/renderer/renderers/paragraph/index.js +1 -2
  85. package/dist/renderer/renderers/person/index.js +1 -2
  86. package/dist/renderer/renderers/poll/index.js +1 -2
  87. package/dist/renderer/renderers/quote/index.js +1 -2
  88. package/dist/renderer/renderers/spoiler/index.js +1 -2
  89. package/dist/renderer/renderers/table/index.js +1 -2
  90. package/dist/renderer/renderers/toggle/index.js +1 -2
  91. package/dist/renderer/renderers/warning/index.js +1 -2
  92. package/package.json +27 -29
  93. package/dist/shared/resolvePath.d.ts +0 -8
  94. package/dist/shared/resolvePath.js +0 -10
@@ -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
- readOnly: deps.readOnly,
47
- emptyText: deps.t('emptyReadonly', 'No image'),
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} onPromptUrl
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
- onPromptUrl: () => void;
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} onPromptUrl
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
- dropdown.classList.remove(CSS.dropdownOpen)
153
- settingsBtn.setAttribute('aria-expanded', 'false')
154
- } else {
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
- dropdown.classList.remove(CSS.dropdownOpen)
178
- settingsBtn.setAttribute('aria-expanded', 'false')
179
- }
180
- }, { signal })
181
-
182
- dropdown.addEventListener('keydown', (event) => {
183
- if (event.key !== 'Escape' || !dropdown.classList.contains(CSS.dropdownOpen)) return
184
- event.preventDefault()
185
- dropdown.classList.remove(CSS.dropdownOpen)
186
- settingsBtn.setAttribute('aria-expanded', 'false')
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.onPromptUrl(); restore() },
272
+ () => { deps.onOpenUrlEditor(); restore() },
268
273
  signal,
269
274
  ))
270
275
 
@@ -1,10 +1,9 @@
1
- import { resolvePath } from '../../shared/resolvePath.js'
2
1
  import { sanitizeUrl, setSafeUrlAttribute } from '../../shared/sanitize/sanitizeUrl.js'
3
2
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
4
3
  import { validateLinkPreviewData } from '../../shared/blockDataValidators.js'
5
4
  import { normalizeTextValue } from '../../shared/textFormat.js'
6
5
 
7
- const editorStyles = resolvePath('./link-preview.css', import.meta.url)
6
+ const editorStyles = new URL('./link-preview.css', import.meta.url).href
8
7
 
9
8
  // Tabler: link (toolbox)
10
9
  const ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 15l6-6"/><path d="M11 6l.463-.536a5 5 0 0 1 7.071 7.072L18 13"/><path d="M13 18l-.397.534a5.068 5.068 0 0 1-7.127 0 4.972 4.972 0 0 1 0-7.071L6 11"/></svg>'
@@ -1,11 +1,10 @@
1
1
  import { sanitizeHtml } from '../../core/sanitize.js'
2
- import { resolvePath } from '../../shared/resolvePath.js'
3
2
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
4
3
  import { mapTextFields } from './mapTextFields.js'
5
4
  import { validateListData } from '../../shared/blockDataValidators.js'
6
5
  import { normalizeTextValue } from '../../shared/textFormat.js'
7
6
 
8
- const editorStyles = resolvePath('./list.css', import.meta.url)
7
+ const editorStyles = new URL('./list.css', import.meta.url).href
9
8
 
10
9
  // Tabler icon: list
11
10
  const ICON_UL = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11"/><path d="M9 12h11"/><path d="M9 18h11"/><path d="M5 6v.01"/><path d="M5 12v.01"/><path d="M5 18v.01"/></svg>'
@@ -1,11 +1,10 @@
1
1
  import { sanitizeHtml } from '../../core/sanitize.js'
2
- import { resolvePath } from '../../shared/resolvePath.js'
3
2
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
4
3
  import { mapTextFields } from './mapTextFields.js'
5
4
  import { validateParagraphData } from '../../shared/blockDataValidators.js'
6
5
  import { normalizeTextAlign, normalizeTextValue } from '../../shared/textFormat.js'
7
6
 
8
- const editorStyles = resolvePath('./paragraph.css', import.meta.url)
7
+ const editorStyles = new URL('./paragraph.css', import.meta.url).href
9
8
 
10
9
  // Tabler icon: letter-t
11
10
  const ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4l12 0"/><path d="M12 4l0 16"/></svg>'
@@ -1,13 +1,12 @@
1
1
  import { sanitizeHtml } from '../../core/sanitize.js'
2
2
  import { CropperDialog, cropperStylesUrl } from '@shelamkoff/cropper'
3
3
  import { resolveSocialIcon, SOCIAL_ICONS } from './socialResolver.js'
4
- import { resolvePath } from '../../shared/resolvePath.js'
5
4
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
6
5
  import { sanitizeUrl, setSafeUrlAttribute } from '../../shared/sanitize/sanitizeUrl.js'
7
6
  import { validatePersonData } from '../../shared/blockDataValidators.js'
8
7
  import { normalizeTextValue } from '../../shared/textFormat.js'
9
8
 
10
- const editorStyles = resolvePath('./person.css', import.meta.url)
9
+ const editorStyles = new URL('./person.css', import.meta.url).href
11
10
  const cropperStyles = cropperStylesUrl
12
11
 
13
12
  // Tabler icon: user-circle
@@ -1,5 +1,4 @@
1
1
  import { sanitizeHtml } from '../../core/sanitize.js'
2
- import { resolvePath } from '../../shared/resolvePath.js'
3
2
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
4
3
  import { uid } from '../../core/uid.js'
5
4
  import {
@@ -11,7 +10,7 @@ import {
11
10
  } from '../../shared/pollData.js'
12
11
  import { setSafeUrlAttribute } from '../../shared/sanitize/sanitizeUrl.js'
13
12
 
14
- const editorStyles = resolvePath('./poll.css', import.meta.url)
13
+ const editorStyles = new URL('./poll.css', import.meta.url).href
15
14
 
16
15
  // Tabler icon: chart-bar
17
16
  const ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/><path d="M9 8m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/><path d="M15 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/><path d="M4 20h14"/></svg>'
@@ -3,13 +3,12 @@
3
3
  // =============================================================================
4
4
 
5
5
  import { sanitizeHtml } from '../../core/sanitize.js'
6
- import { resolvePath } from '../../shared/resolvePath.js'
7
6
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
8
7
  import { mapTextFields } from './mapTextFields.js'
9
8
  import { validateQuoteData } from '../../shared/blockDataValidators.js'
10
9
  import { normalizeTextValue } from '../../shared/textFormat.js'
11
10
 
12
- const editorStyles = resolvePath('./quote.css', import.meta.url)
11
+ const editorStyles = new URL('./quote.css', import.meta.url).href
13
12
 
14
13
  // Tabler icon: blockquote
15
14
  const ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/><path d="M18 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/></svg>'
@@ -1,10 +1,9 @@
1
- import { resolvePath } from '../../shared/resolvePath.js'
2
1
  import { BlockPluginAbstract } from '../BlockPluginAbstract.js'
3
2
  import { validateRawData } from '../../shared/blockDataValidators.js'
4
3
  import { normalizeTextValue } from '../../shared/textFormat.js'
5
4
  import { sanitizeRawHtml } from '../../shared/sanitize/sanitizeRawHtml.js'
6
5
 
7
- const editorStyles = resolvePath('./raw.css', import.meta.url)
6
+ const editorStyles = new URL('./raw.css', import.meta.url).href
8
7
 
9
8
  const ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 16v-8l2 5l2-5v8"/><path d="M1 16v-8"/><path d="M5 8v8"/><path d="M1 12h4"/><path d="M7 8h4"/><path d="M9 8v8"/><path d="M20 8v8h3"/></svg>'
10
9
 
@@ -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<{ icon?: string, label: string, onSelect: () => void }>} [actions]
26
- * @property {boolean} [readOnly] Render a non-interactive empty state.
27
- * @property {string} [emptyText] Text shown for an empty read-only block.
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
- const afterText = document.createTextNode(' ' + config.afterText)
71
-
72
- text.append(uploadLink, afterText)
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) {