@tiny-mce-umbraco/backoffice 17.3.4 → 17.6.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/dist-cms/README.md +1 -0
- package/dist-cms/components/input-tiny-mce/input-tiny-mce.defaults.d.ts.map +1 -1
- package/dist-cms/components/input-tiny-mce/input-tiny-mce.defaults.js +175 -6
- package/dist-cms/components/input-tiny-mce/input-tiny-mce.element.d.ts +4 -1
- package/dist-cms/components/input-tiny-mce/input-tiny-mce.element.d.ts.map +1 -1
- package/dist-cms/components/input-tiny-mce/input-tiny-mce.element.js +97 -12
- package/dist-cms/components/input-tiny-mce/shadow-dom-selection.d.ts +16 -0
- package/dist-cms/components/input-tiny-mce/shadow-dom-selection.d.ts.map +1 -0
- package/dist-cms/components/input-tiny-mce/shadow-dom-selection.js +112 -0
- package/dist-cms/components/stylesheet-rule-input/stylesheet-rule-input.element.d.ts +1 -1
- package/dist-cms/entry-points/blob-download-router-fix.d.ts +56 -0
- package/dist-cms/entry-points/blob-download-router-fix.d.ts.map +1 -0
- package/dist-cms/entry-points/blob-download-router-fix.js +81 -0
- package/dist-cms/entry-points/entry-point.d.ts.map +1 -1
- package/dist-cms/entry-points/entry-point.js +7 -1
- package/dist-cms/localizations/ar.d.ts.map +1 -1
- package/dist-cms/localizations/ar.js +3 -0
- package/dist-cms/localizations/bs.d.ts.map +1 -1
- package/dist-cms/localizations/bs.js +3 -0
- package/dist-cms/localizations/cy.d.ts.map +1 -1
- package/dist-cms/localizations/cy.js +3 -0
- package/dist-cms/localizations/da.d.ts.map +1 -1
- package/dist-cms/localizations/da.js +4 -0
- package/dist-cms/localizations/en.d.ts.map +1 -1
- package/dist-cms/localizations/en.js +4 -0
- package/dist-cms/localizations/hr.d.ts.map +1 -1
- package/dist-cms/localizations/hr.js +3 -0
- package/dist-cms/localizations/it.d.ts.map +1 -1
- package/dist-cms/localizations/it.js +3 -0
- package/dist-cms/localizations/manifests.d.ts.map +1 -1
- package/dist-cms/localizations/manifests.js +24 -24
- package/dist-cms/localizations/nl.d.ts.map +1 -1
- package/dist-cms/localizations/nl.js +3 -0
- package/dist-cms/localizations/tr.d.ts.map +1 -1
- package/dist-cms/localizations/tr.js +3 -0
- package/dist-cms/plugins/manifests.d.ts.map +1 -1
- package/dist-cms/plugins/manifests.js +17 -0
- package/dist-cms/plugins/tiny-mce-block-picker.plugin.d.ts.map +1 -1
- package/dist-cms/plugins/tiny-mce-block-picker.plugin.js +20 -2
- package/dist-cms/plugins/tiny-mce-embeddedmedia.plugin.d.ts.map +1 -1
- package/dist-cms/plugins/tiny-mce-embeddedmedia.plugin.js +1 -0
- package/dist-cms/plugins/tiny-mce-mediapicker.plugin.d.ts.map +1 -1
- package/dist-cms/plugins/tiny-mce-mediapicker.plugin.js +127 -43
- package/dist-cms/property-editors/configEditor/property-editor-ui-tiny-mce-config-editor.element.d.ts.map +1 -1
- package/dist-cms/property-editors/configEditor/property-editor-ui-tiny-mce-config-editor.element.js +0 -1
- package/dist-cms/property-editors/manifests.js +3 -3
- package/dist-cms/property-editors/plugin/property-editor-ui-tiny-mce-plugin-configuration.element.d.ts.map +1 -1
- package/dist-cms/property-editors/plugin/property-editor-ui-tiny-mce-plugin-configuration.element.js +15 -5
- package/dist-cms/property-editors/tiny-mce/manifests.d.ts.map +1 -1
- package/dist-cms/property-editors/tiny-mce/manifests.js +2 -1
- package/dist-cms/utils/index.d.ts +15 -5
- package/dist-cms/utils/index.d.ts.map +1 -1
- package/dist-cms/utils/index.js +27 -5
- package/package.json +6 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This is an Umbraco CMS package that enables access to the paid features in the TinyMCE-based Rich Text Editor (RTE) in version 12+ with a license. It also adds a new TinyMCE Premium property editor with additional settings, providing a more targeted configuration setup for RTE Data Types in Umbraco. Finally, it allows for additional settings that support JSON directly in the configuration for .NET (appsettings.config).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-tiny-mce.defaults.d.ts","sourceRoot":"","sources":["../../../src/components/input-tiny-mce/input-tiny-mce.defaults.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;
|
|
1
|
+
{"version":3,"file":"input-tiny-mce.defaults.d.ts","sourceRoot":"","sources":["../../../src/components/input-tiny-mce/input-tiny-mce.defaults.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAKjF,eAAO,MAAM,4CAA4C,sCAAsC,CAAC;AAEhG,eAAO,MAAM,yBAAyB,uQAmB5B,CAAC;AAMX,eAAO,MAAM,qBAAqB,EAAE,gBA0UnC,CAAC"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { UMB_CONTEXT_REQUEST_EVENT_TYPE } from '@umbraco-cms/backoffice/context-api';
|
|
2
|
+
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
|
2
3
|
import { UUIIconRequestEvent } from '@umbraco-cms/backoffice/external/uui';
|
|
4
|
+
import { umbLocalizationManager } from '@umbraco-cms/backoffice/localization-api';
|
|
3
5
|
export const UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH = '@umbraco-cms/backoffice/block-rte';
|
|
4
6
|
export const defaultPremiumPluginsList = [
|
|
5
7
|
'a11ychecker',
|
|
@@ -27,9 +29,9 @@ export const defaultPremiumPluginsList = [
|
|
|
27
29
|
//if a user removes an element from ValidElements config it would still be allowed via this list.
|
|
28
30
|
export const defaultFallbackConfig = {
|
|
29
31
|
plugins: ['anchor', 'charmap', 'table', 'lists', 'advlist', 'autolink', 'directionality', 'searchreplace'],
|
|
30
|
-
valid_elements: '+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style|lang],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*],details[*],summary[*],-code',
|
|
32
|
+
valid_elements: '+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style|contenteditable|data-embed-url|data-embed-height|data-embed-width|data-embed-constrain],-span[class|align|style|lang],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*],details[*],summary[*],-code',
|
|
31
33
|
invalid_elements: 'font',
|
|
32
|
-
extended_valid_elements: '@[id|class|style],+umb-rte-block[!data-content-key],+umb-rte-block-inline[!data-content-key],ins[datetime|cite],figure,figcaption',
|
|
34
|
+
extended_valid_elements: '@[id|class|style],+umb-rte-block[!data-content-key],+umb-rte-block-inline[!data-content-key],ins[datetime|cite],figure,figcaption,iframe[*]',
|
|
33
35
|
custom_elements: 'umb-rte-block,~umb-rte-block-inline',
|
|
34
36
|
toolbar: [
|
|
35
37
|
'styles',
|
|
@@ -57,6 +59,12 @@ export const defaultFallbackConfig = {
|
|
|
57
59
|
// });
|
|
58
60
|
//},
|
|
59
61
|
init_instance_callback: function (editor) {
|
|
62
|
+
// Everything below bootstraps the editor's *iframe* realm through `editor.dom.doc.head`. Inline
|
|
63
|
+
// mode has no iframe and `editor.dom.doc` IS the backoffice document, so running any of it would
|
|
64
|
+
// inject a duplicate import map and re-register live manifests into the running registry - and
|
|
65
|
+
// none of it is needed there, since the components it bootstraps already live in this realm.
|
|
66
|
+
if (!editor.iframeElement)
|
|
67
|
+
return;
|
|
60
68
|
// The following code is the context api proxy. [NL]
|
|
61
69
|
// It re-dispatches the context api request event to the origin target of this modal, in other words the element that initiated the modal. [NL]
|
|
62
70
|
editor.dom.doc.addEventListener(UMB_CONTEXT_REQUEST_EVENT_TYPE, ((event) => {
|
|
@@ -124,12 +132,173 @@ export const defaultFallbackConfig = {
|
|
|
124
132
|
// Change the history!
|
|
125
133
|
window.history.pushState(null, '', path);
|
|
126
134
|
});
|
|
127
|
-
//
|
|
135
|
+
// Sync block editor localization keys into the iframe's module-scope umbLocalizationManager.
|
|
136
|
+
// The iframe has a completely separate module scope (and thus a separate umbLocalizationManager
|
|
137
|
+
// instance) from the outer document. Without this, requestDelete() in UmbBlockEntryContext
|
|
138
|
+
// falls back to returning raw keys because the iframe's manager has no registered translations.
|
|
139
|
+
const locSetsToSync = [];
|
|
140
|
+
// Keys needed by components that render INSIDE the TinyMCE iframe. The iframe has its own
|
|
141
|
+
// per-realm umbLocalizationManager, so anything not copied across renders blank there.
|
|
142
|
+
// The unsupported-block pair is read by Umbraco 17.6's <umb-unsupported-rte-block>.
|
|
143
|
+
const BLOCK_LOC_KEYS = [
|
|
144
|
+
// Rendered by the block components themselves.
|
|
145
|
+
'blockEditor_confirmDeleteBlockTitle',
|
|
146
|
+
'blockEditor_confirmDeleteBlockMessage',
|
|
147
|
+
'blockEditor_unsupportedBlockName',
|
|
148
|
+
'blockEditor_unsupportedBlockDescription',
|
|
149
|
+
// Labels declared by Umbraco's `blockAction` manifests, which `umb-block-action-list`
|
|
150
|
+
// renders inside the iframe. Enumerated from
|
|
151
|
+
// `packages/block/block/action/common/*/manifests.ts` — kept complete deliberately, so a
|
|
152
|
+
// newly reachable action does not show a raw `#key` the way copy-to-clipboard did.
|
|
153
|
+
'clipboard_labelForCopyToClipboard',
|
|
154
|
+
'general_delete',
|
|
155
|
+
'general_edit',
|
|
156
|
+
'general_settings',
|
|
157
|
+
'actions_create',
|
|
158
|
+
];
|
|
159
|
+
umbLocalizationManager.localizations.forEach((locSet, code) => {
|
|
160
|
+
const locSetAny = locSet;
|
|
161
|
+
const entry = {
|
|
162
|
+
$code: code,
|
|
163
|
+
$dir: locSetAny['$dir'] ?? 'ltr',
|
|
164
|
+
};
|
|
165
|
+
let hasKey = false;
|
|
166
|
+
for (const key of BLOCK_LOC_KEYS) {
|
|
167
|
+
const val = locSetAny[key];
|
|
168
|
+
if (typeof val === 'string') {
|
|
169
|
+
entry[key] = val;
|
|
170
|
+
hasKey = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (hasKey)
|
|
174
|
+
locSetsToSync.push(entry);
|
|
175
|
+
});
|
|
176
|
+
// The active language must cross the realm boundary too, not just the dictionaries.
|
|
177
|
+
// Without this the iframe resolves every term through UmbLocalizationController's
|
|
178
|
+
// primary -> secondary -> `en` fallback chain, so the synced Danish set sits in the
|
|
179
|
+
// iframe's map unused and the placeholder renders in English.
|
|
180
|
+
//
|
|
181
|
+
// This must go through `umbLocalizationRegistry.loadLanguage()`, not a direct write to
|
|
182
|
+
// `umbLocalizationManager.documentLanguage`. The registry is a module side-effect
|
|
183
|
+
// (`export const umbLocalizationRegistry = new UmbLocalizationRegistry(...)`), and the
|
|
184
|
+
// block script below imports `@umbraco-cms/backoffice/block-rte`, whose module graph pulls
|
|
185
|
+
// the localization package into the iframe realm. Constructing the registry there emits
|
|
186
|
+
// its `UMB_DEFAULT_LOCALIZATION_CULTURE` bootstrap value synchronously, whose `tap` stamps
|
|
187
|
+
// `'en'` onto the manager — overwriting a direct assignment. Both scripts are dynamically
|
|
188
|
+
// inserted and therefore `async` with no guaranteed order, and the block script's larger
|
|
189
|
+
// graph reliably lands last. Importing the registry here instead means it is constructed
|
|
190
|
+
// before this script's body runs, so `loadLanguage()` always wins regardless of order.
|
|
191
|
+
// Nothing inside the iframe ever calls `loadLanguage()` otherwise — that is driven by
|
|
192
|
+
// `<umb-app>` and the current-user context, neither of which exists in this realm.
|
|
193
|
+
const outerLang = umbLocalizationManager.documentLanguage;
|
|
194
|
+
const outerDir = umbLocalizationManager.documentDirection;
|
|
195
|
+
if (locSetsToSync.length > 0) {
|
|
196
|
+
const locScript = document.createElement('script');
|
|
197
|
+
locScript.setAttribute('type', 'module');
|
|
198
|
+
locScript.text = `
|
|
199
|
+
import { umbLocalizationManager } from "@umbraco-cms/backoffice/localization-api";
|
|
200
|
+
import { umbLocalizationRegistry } from "@umbraco-cms/backoffice/localization";
|
|
201
|
+
|
|
202
|
+
// Sets documentLanguage via the registry's synchronous \`tap\`. The load that
|
|
203
|
+
// follows finds no localization extensions in this realm's registry, so the
|
|
204
|
+
// pipeline stops at its own length guard and never writes the language again.
|
|
205
|
+
umbLocalizationRegistry.loadLanguage(${JSON.stringify(outerLang)});
|
|
206
|
+
// Direction is only set by #setBrowserLanguage, which that stalled pipeline never
|
|
207
|
+
// reaches, so carry it across by hand — it matters for an RTL backoffice.
|
|
208
|
+
umbLocalizationManager.documentDirection = ${JSON.stringify(outerDir)};
|
|
209
|
+
// Register last: registerLocalization's keysChanged pass is what re-renders any
|
|
210
|
+
// consumer that connected before this script ran, and setting the language
|
|
211
|
+
// notifies nobody on its own.
|
|
212
|
+
${JSON.stringify(locSetsToSync)}.forEach(s => umbLocalizationManager.registerLocalization(s));
|
|
213
|
+
`;
|
|
214
|
+
editor.dom.doc.head.appendChild(locScript);
|
|
215
|
+
}
|
|
216
|
+
// Expose the outer extension registry on the outer window so the injected module
|
|
217
|
+
// script below can sync blockAction manifests into the iframe's per-realm instance.
|
|
218
|
+
// umb-block-action-list reads umbExtensionsRegistry as a module-level singleton
|
|
219
|
+
// (not via context), so the context proxy alone cannot bridge it across realms.
|
|
220
|
+
window._umbOuterExtReg = umbExtensionsRegistry;
|
|
221
|
+
// Load backoffice JS so we can get the umb-rte-block component registered inside the iframe.
|
|
222
|
+
// Also:
|
|
223
|
+
// 1. Import @umbraco-cms/backoffice/block to register the blockAction kind definition
|
|
224
|
+
// and the <umb-block-action> custom element in the iframe's realm. These are NOT
|
|
225
|
+
// transitively imported by block-rte, but umb-block-action-list needs them to
|
|
226
|
+
// instantiate block action extensions (kind: 'default').
|
|
227
|
+
// 2. Sync blockAction manifests from the outer registry into the iframe's registry.
|
|
228
|
+
// Umbraco registers manifests via umbraco-package.json (not module imports), so they
|
|
229
|
+
// only exist in the outer registry — we must copy them into the iframe's per-realm
|
|
230
|
+
// umbExtensionsRegistry so umb-block-action-list can discover them.
|
|
128
231
|
const script = document.createElement('script');
|
|
129
|
-
script.type = 'text/javascript';
|
|
130
232
|
script.setAttribute('type', 'module');
|
|
131
|
-
script.text = `
|
|
132
|
-
|
|
233
|
+
script.text = `
|
|
234
|
+
import { umbExtensionsRegistry } from "@umbraco-cms/backoffice/extension-registry";
|
|
235
|
+
import { UMB_BLOCK_ACTION_DEFAULT_KIND_MANIFEST } from "@umbraco-cms/backoffice/block";
|
|
236
|
+
import "${UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH}";
|
|
237
|
+
// Defines <umb-icon>, which umb-ref-rte-block renders for the block's element-type icon.
|
|
238
|
+
// block-rte does not pull it in transitively, so without this it stays an un-upgraded
|
|
239
|
+
// unknown element: the .name property binding still lands, but nothing ever renders.
|
|
240
|
+
// The existing UUIIconRequestEvent proxy is not involved — uui-icon is already defined
|
|
241
|
+
// in this realm, and the action-bar icons resolve through that proxy correctly.
|
|
242
|
+
// This is the whole components barrel because the export map exposes no narrower path.
|
|
243
|
+
import "@umbraco-cms/backoffice/components";
|
|
244
|
+
|
|
245
|
+
// Register the blockAction default kind definition in the inner realm's registry.
|
|
246
|
+
// UMB_BLOCK_ACTION_DEFAULT_KIND_MANIFEST is imported here (inner realm), so its
|
|
247
|
+
// element factory — element: () => import('./block-action.element.js') — captures
|
|
248
|
+
// the inner realm's module URL. When the extension system later calls that factory,
|
|
249
|
+
// block-action.element.js is loaded in the inner realm, registering <umb-block-action>
|
|
250
|
+
// in the inner customElements registry.
|
|
251
|
+
umbExtensionsRegistry.registerMany([UMB_BLOCK_ACTION_DEFAULT_KIND_MANIFEST]);
|
|
252
|
+
|
|
253
|
+
// Sync blockAction manifests from the outer registry into the inner realm's registry.
|
|
254
|
+
// Umbraco registers these via umbraco-package.json at backoffice startup; they only
|
|
255
|
+
// exist in the outer registry and must be copied so umb-block-action-list can discover them.
|
|
256
|
+
const outerReg = window.parent?._umbOuterExtReg;
|
|
257
|
+
if (outerReg) {
|
|
258
|
+
const blockActions = outerReg.getByType('blockAction') ?? [];
|
|
259
|
+
if (blockActions.length) umbExtensionsRegistry.registerMany(blockActions);
|
|
260
|
+
|
|
261
|
+
// Block actions declare their \`conditions\` by alias, and the extension system resolves
|
|
262
|
+
// each alias against the registry the action was registered in. Copying the actions
|
|
263
|
+
// without the condition manifests they name leaves every conditional action permanently
|
|
264
|
+
// un-initialized — delete, edit content, edit settings and expose content all have
|
|
265
|
+
// conditions, copy-to-clipboard does not, which is why it was the only one that rendered.
|
|
266
|
+
// Derived from the copied manifests rather than hard-coded, so an action that gains a
|
|
267
|
+
// new condition in a future Umbraco version is covered without another fix here.
|
|
268
|
+
const conditionAliases = new Set(
|
|
269
|
+
blockActions.flatMap((a) => (a.conditions ?? []).map((c) => c.alias)),
|
|
270
|
+
);
|
|
271
|
+
const conditions = (outerReg.getByType('condition') ?? []).filter((c) =>
|
|
272
|
+
conditionAliases.has(c.alias),
|
|
273
|
+
);
|
|
274
|
+
if (conditions.length) umbExtensionsRegistry.registerMany(conditions);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Define the ufm-* elements in the inner realm so {=alias} block labels interpolate. Unlike
|
|
278
|
+
// umb-icon these are in no importable barrel - the outer document defines them by running
|
|
279
|
+
// each ufmComponent manifest's api(). Two constraints when editing this:
|
|
280
|
+
// 1. The manifests are on the package's \`manifests\` export, NOT \`extensions\`, which is a
|
|
281
|
+
// single bundle wrapper - filtering that matches nothing and fails silently.
|
|
282
|
+
// 2. This is the only top-level await in the script, so it must stay LAST or everything
|
|
283
|
+
// above it waits on the ufm package plus one dynamic import per component. Custom
|
|
284
|
+
// element upgrade is retroactive, so defining these last costs nothing.
|
|
285
|
+
try {
|
|
286
|
+
const ufmPkg = await import("/umbraco/backoffice/packages/ufm/umbraco-package.js");
|
|
287
|
+
const loaded = new Set();
|
|
288
|
+
for (const manifest of (ufmPkg.manifests ?? []).filter((m) => m.type === "ufmComponent")) {
|
|
289
|
+
if (loaded.has(manifest.alias)) continue;
|
|
290
|
+
loaded.add(manifest.alias);
|
|
291
|
+
try {
|
|
292
|
+
await manifest.api?.();
|
|
293
|
+
} catch {
|
|
294
|
+
// A single component failing to load should not break the rest.
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// If the ufm package is ever restructured, block labels degrade back to empty
|
|
299
|
+
// rather than breaking the editor.
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
133
302
|
editor.dom.doc.head.appendChild(script);
|
|
134
303
|
},
|
|
135
304
|
style_formats: [
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit';
|
|
1
2
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
|
2
3
|
import type { Editor } from '@tiny-mce-umbraco/backoffice/external/tinymce';
|
|
3
4
|
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
|
|
4
|
-
declare const UmbInputTinyMceElement_base: (new (...args: any[]) => import("@umbraco-ui/uui
|
|
5
|
+
declare const UmbInputTinyMceElement_base: (new (...args: any[]) => import("@umbraco-ui/uui").UUIFormControlWithBasicsMixinElement<FormDataEntryValue | FormData>) & typeof UmbLitElement;
|
|
5
6
|
export declare class UmbInputTinyMceElement extends UmbInputTinyMceElement_base {
|
|
6
7
|
#private;
|
|
7
8
|
configuration?: UmbPropertyEditorConfigCollection;
|
|
@@ -17,7 +18,9 @@ export declare class UmbInputTinyMceElement extends UmbInputTinyMceElement_base
|
|
|
17
18
|
get readonly(): boolean;
|
|
18
19
|
set readonly(value: boolean);
|
|
19
20
|
private readonly _editorElement?;
|
|
21
|
+
private _inline;
|
|
20
22
|
getEditor(): Editor | null | undefined;
|
|
23
|
+
willUpdate(changedProperties: PropertyValues): void;
|
|
21
24
|
firstUpdated(): void;
|
|
22
25
|
disconnectedCallback(): void;
|
|
23
26
|
getFormatStyles(stylesheetPaths: Array<string>): Promise<any[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-tiny-mce.element.d.ts","sourceRoot":"","sources":["../../../src/components/input-tiny-mce/input-tiny-mce.element.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"input-tiny-mce.element.d.ts","sourceRoot":"","sources":["../../../src/components/input-tiny-mce/input-tiny-mce.element.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAO3E,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAKpE,OAAO,KAAK,EAAe,MAAM,EAAoB,MAAM,+CAA+C,CAAC;AAE3G,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,yCAAyC,CAAC;;AA6EjG,qBACa,sBAAuB,SAAQ,2BAAsC;;IAEjF,aAAa,CAAC,EAAE,iCAAiC,CAAC;cAe/B,cAAc;IAIjC,IAAa,KAAK,CAAC,QAAQ,EAAE,kBAAkB,GAAG,QAAQ,EAOzD;IAED,IAAa,KAAK,IAAI,kBAAkB,GAAG,QAAQ,CAElD;IAED;;;;;OAKG;IACH,IACW,QAAQ,YAElB;IACD,IAAW,QAAQ,CAAC,KAAK,SAAA,EAOxB;IAID,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAc;IAG9C,OAAO,CAAC,OAAO,CAAS;IAExB,SAAS;IAIA,UAAU,CAAC,iBAAiB,EAAE,cAAc;IAU5C,YAAY;IA+BZ,oBAAoB;IA0BvB,eAAe,CAAC,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC;IAiXpD;;;OAGG;IACM,MAAM;IAMf,gBAAyB,MAAM,4BAiC7B;CACF;AAED,eAAe,sBAAsB,CAAC;AAEtC,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,qBAAqB;QAC9B,oBAAoB,EAAE,sBAAsB,CAAC;KAC7C;CACD"}
|
|
@@ -15,13 +15,13 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
15
15
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
16
16
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
17
17
|
};
|
|
18
|
-
var _UmbInputTinyMceElement_instances, _UmbInputTinyMceElement_plugins, _UmbInputTinyMceElement_editorRef, _UmbInputTinyMceElement_sanitizeTinyMce, _UmbInputTinyMceElement_stylesheetRepository, _UmbInputTinyMceElement_umbStylesheetRuleManager, _UmbInputTinyMceElement_readonly, _UmbInputTinyMceElement_loadEditor, _UmbInputTinyMceElement_loadPlugins, _UmbInputTinyMceElement_getTinyMceConfig, _UmbInputTinyMceElement_setTinyConfig, _UmbInputTinyMceElement_getLanguage, _UmbInputTinyMceElement_editorSetup, _UmbInputTinyMceElement_onInit, _UmbInputTinyMceElement_onChange;
|
|
18
|
+
var _UmbInputTinyMceElement_instances, _UmbInputTinyMceElement_plugins, _UmbInputTinyMceElement_editorRef, _UmbInputTinyMceElement_sanitizeTinyMce, _UmbInputTinyMceElement_stylesheetRootPath, _UmbInputTinyMceElement_stylesheetRepository, _UmbInputTinyMceElement_umbStylesheetRuleManager, _UmbInputTinyMceElement_readonly, _UmbInputTinyMceElement_loadStylesheetRootPath, _UmbInputTinyMceElement_loadEditor, _UmbInputTinyMceElement_loadPlugins, _UmbInputTinyMceElement_getTinyMceConfig, _UmbInputTinyMceElement_setTinyConfig, _UmbInputTinyMceElement_getLanguage, _UmbInputTinyMceElement_editorSetup, _UmbInputTinyMceElement_onInit, _UmbInputTinyMceElement_onChange;
|
|
19
19
|
import { availableLanguages } from '@tiny-mce-umbraco/backoffice/core';
|
|
20
20
|
import { defaultFallbackConfig, defaultPremiumPluginsList } from '@tiny-mce-umbraco/backoffice/core';
|
|
21
21
|
import { pastePreProcessHandler } from '@tiny-mce-umbraco/backoffice/core';
|
|
22
22
|
import { uriAttributeSanitizer } from '@tiny-mce-umbraco/backoffice/core';
|
|
23
23
|
import { UmbStylesheetRuleManager } from '@tiny-mce-umbraco/backoffice/core';
|
|
24
|
-
import { css, customElement, html, property, query } from '@umbraco-cms/backoffice/external/lit';
|
|
24
|
+
import { css, customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit';
|
|
25
25
|
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
|
26
26
|
import { getProcessedImageUrl, umbDeepMerge } from '@umbraco-cms/backoffice/utils';
|
|
27
27
|
import { renderEditor } from '@umbraco-cms/backoffice/external/tinymce';
|
|
@@ -30,7 +30,9 @@ import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api
|
|
|
30
30
|
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
|
31
31
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
|
32
32
|
import { UmbStylesheetDetailRepository } from '@umbraco-cms/backoffice/stylesheet';
|
|
33
|
+
import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server';
|
|
33
34
|
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
|
35
|
+
import { bridgeInlineEditor } from './shadow-dom-selection.js';
|
|
34
36
|
import { TinyMceService } from '../../api/index.js';
|
|
35
37
|
import { tryExecute } from '@umbraco-cms/backoffice/resources';
|
|
36
38
|
// @ts-ignore
|
|
@@ -52,10 +54,24 @@ async function onResize(e) {
|
|
|
52
54
|
});
|
|
53
55
|
e.target.setAttribute('data-mce-src', resizedPath);
|
|
54
56
|
}
|
|
57
|
+
// Merges plugin lists into the flat `string[]` TinyMCE expects, dropping anything that is not a
|
|
58
|
+
// string. That filtering is load-bearing, not defensive dressing: TinyMCE trims every plugin name with
|
|
59
|
+
// `String.replace` (`trim$4`, called from its plugin-list parser), so a single non-string entry throws
|
|
60
|
+
// during editor construction and the editor never renders at all.
|
|
61
|
+
//
|
|
62
|
+
// Non-strings do reach here from configuration. `Umbraco:CMS:RichTextEditor:CustomConfig` binds into an
|
|
63
|
+
// `IDictionary<string, object>`, so a value written as a real JSON array has no scalar to bind and
|
|
64
|
+
// arrives as an object rather than a list. The supported way to express a list there is a JSON *string*
|
|
65
|
+
// - `"plugins": "[\"fullscreen\"]"` - which `parseJsonStringValues` turns back into a real array.
|
|
55
66
|
function mergeArrays(arr1, arr2) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
const toStringList = (value) => {
|
|
68
|
+
if (typeof value === 'string')
|
|
69
|
+
return [value];
|
|
70
|
+
if (!Array.isArray(value))
|
|
71
|
+
return [];
|
|
72
|
+
return value.filter((entry) => typeof entry === 'string');
|
|
73
|
+
};
|
|
74
|
+
return Array.from(new Set([...toStringList(arr1), ...toStringList(arr2)]));
|
|
59
75
|
}
|
|
60
76
|
// Some config paths (e.g. Umbraco:CMS:RichTextEditor:CustomConfig) can only store scalar string
|
|
61
77
|
// values via the .NET config binder. Complex TinyMCE options like style_formats were historically
|
|
@@ -86,9 +102,15 @@ let UmbInputTinyMceElement = class UmbInputTinyMceElement extends UUIFormControl
|
|
|
86
102
|
_UmbInputTinyMceElement_plugins.set(this, []);
|
|
87
103
|
_UmbInputTinyMceElement_editorRef.set(this, null);
|
|
88
104
|
_UmbInputTinyMceElement_sanitizeTinyMce.set(this, true);
|
|
105
|
+
/**
|
|
106
|
+
* Where the picked stylesheets are served from. Configurable as `Global:UmbracoCssPath`; `/css` is
|
|
107
|
+
* the fallback Umbraco itself uses when the server has not reported one yet.
|
|
108
|
+
*/
|
|
109
|
+
_UmbInputTinyMceElement_stylesheetRootPath.set(this, '/css');
|
|
89
110
|
_UmbInputTinyMceElement_stylesheetRepository.set(this, new UmbStylesheetDetailRepository(this));
|
|
90
111
|
_UmbInputTinyMceElement_umbStylesheetRuleManager.set(this, new UmbStylesheetRuleManager());
|
|
91
112
|
_UmbInputTinyMceElement_readonly.set(this, false);
|
|
113
|
+
this._inline = false;
|
|
92
114
|
}
|
|
93
115
|
getFormElement() {
|
|
94
116
|
return this._editorElement?.querySelector('iframe') ?? undefined;
|
|
@@ -123,7 +145,16 @@ let UmbInputTinyMceElement = class UmbInputTinyMceElement extends UUIFormControl
|
|
|
123
145
|
getEditor() {
|
|
124
146
|
return __classPrivateFieldGet(this, _UmbInputTinyMceElement_editorRef, "f");
|
|
125
147
|
}
|
|
148
|
+
willUpdate(changedProperties) {
|
|
149
|
+
super.willUpdate(changedProperties);
|
|
150
|
+
// Resolved before the first render so the target element carries the right classes from the
|
|
151
|
+
// start - TinyMCE takes it over during init and `render()` cannot usefully re-run after that.
|
|
152
|
+
if (changedProperties.has('configuration')) {
|
|
153
|
+
this._inline = this.configuration?.getValueByAlias('mode')?.toLocaleLowerCase() === 'inline';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
126
156
|
firstUpdated() {
|
|
157
|
+
__classPrivateFieldGet(this, _UmbInputTinyMceElement_instances, "m", _UmbInputTinyMceElement_loadStylesheetRootPath).call(this);
|
|
127
158
|
__classPrivateFieldGet(this, _UmbInputTinyMceElement_instances, "m", _UmbInputTinyMceElement_loadEditor).call(this);
|
|
128
159
|
}
|
|
129
160
|
disconnectedCallback() {
|
|
@@ -177,16 +208,27 @@ let UmbInputTinyMceElement = class UmbInputTinyMceElement extends UUIFormControl
|
|
|
177
208
|
* a target div and binds the RTE to that element
|
|
178
209
|
*/
|
|
179
210
|
render() {
|
|
180
|
-
|
|
211
|
+
// `umb-rte` stands in for `body_class`, which TinyMCE only applies to the iframe body - in
|
|
212
|
+
// inline mode this element *is* the editable body.
|
|
213
|
+
return html `<div class=${this._inline ? 'editor inline umb-rte' : 'editor'}></div>`;
|
|
181
214
|
}
|
|
182
215
|
};
|
|
183
216
|
_UmbInputTinyMceElement_plugins = new WeakMap();
|
|
184
217
|
_UmbInputTinyMceElement_editorRef = new WeakMap();
|
|
185
218
|
_UmbInputTinyMceElement_sanitizeTinyMce = new WeakMap();
|
|
219
|
+
_UmbInputTinyMceElement_stylesheetRootPath = new WeakMap();
|
|
186
220
|
_UmbInputTinyMceElement_stylesheetRepository = new WeakMap();
|
|
187
221
|
_UmbInputTinyMceElement_umbStylesheetRuleManager = new WeakMap();
|
|
188
222
|
_UmbInputTinyMceElement_readonly = new WeakMap();
|
|
189
223
|
_UmbInputTinyMceElement_instances = new WeakSet();
|
|
224
|
+
_UmbInputTinyMceElement_loadStylesheetRootPath = function _UmbInputTinyMceElement_loadStylesheetRootPath() {
|
|
225
|
+
this.consumeContext(UMB_SERVER_CONTEXT, (serverContext) => {
|
|
226
|
+
this.observe(serverContext?.getServerConnection()?.umbracoCssPath, (umbracoCssPath) => {
|
|
227
|
+
if (umbracoCssPath)
|
|
228
|
+
__classPrivateFieldSet(this, _UmbInputTinyMceElement_stylesheetRootPath, umbracoCssPath, "f");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
};
|
|
190
232
|
_UmbInputTinyMceElement_loadEditor = async function _UmbInputTinyMceElement_loadEditor() {
|
|
191
233
|
this.observe(umbExtensionsRegistry.byType('tinyMcePlugin'), async (manifests) => {
|
|
192
234
|
__classPrivateFieldGet(this, _UmbInputTinyMceElement_plugins, "f").length = 0;
|
|
@@ -234,8 +276,16 @@ _UmbInputTinyMceElement_setTinyConfig = async function _UmbInputTinyMceElement_s
|
|
|
234
276
|
const stylesheetPaths = this.configuration?.getValueByAlias('stylesheets') ?? [];
|
|
235
277
|
const styleFormats = await this.getFormatStyles(stylesheetPaths);
|
|
236
278
|
const preValueCustomConfig = this.configuration?.getValueByAlias('customConfig') ?? {};
|
|
237
|
-
// Map the stylesheets with server url
|
|
238
|
-
|
|
279
|
+
// Map the stylesheets with server url, matching what Umbraco's own rich text editor does in
|
|
280
|
+
// `input-tiptap.element.ts`. Prefixing unconditionally is wrong in two cases: a path that is
|
|
281
|
+
// already rooted (older configurations stored the full `/css/foo.css`, which became
|
|
282
|
+
// `/css/css/foo.css` and 404'd), and an absolute URL to an external stylesheet.
|
|
283
|
+
const stylesheets = stylesheetPaths?.map((stylesheetPath) => {
|
|
284
|
+
const path = stylesheetPath.replace(/\\/g, '/');
|
|
285
|
+
return path.startsWith('http') || path.startsWith(__classPrivateFieldGet(this, _UmbInputTinyMceElement_stylesheetRootPath, "f"))
|
|
286
|
+
? path
|
|
287
|
+
: `${__classPrivateFieldGet(this, _UmbInputTinyMceElement_stylesheetRootPath, "f")}${path}`;
|
|
288
|
+
}) ?? [];
|
|
239
289
|
stylesheets.push('/umbraco/backoffice/css/rte-content.css');
|
|
240
290
|
const appSettingsConfig = await __classPrivateFieldGet(this, _UmbInputTinyMceElement_instances, "m", _UmbInputTinyMceElement_getTinyMceConfig).call(this);
|
|
241
291
|
let apiKey = 'no-origin';
|
|
@@ -300,9 +350,14 @@ _UmbInputTinyMceElement_setTinyConfig = async function _UmbInputTinyMceElement_s
|
|
|
300
350
|
configurationOptions.toolbar = false;
|
|
301
351
|
}
|
|
302
352
|
// set the configured inline mode
|
|
303
|
-
|
|
304
|
-
if (mode?.toLocaleLowerCase() === 'inline') {
|
|
353
|
+
if (this._inline) {
|
|
305
354
|
configurationOptions.inline = true;
|
|
355
|
+
// Inline mode makes the target element itself the editable body, so there is no editor
|
|
356
|
+
// chrome to size and TinyMCE drops height/width on the floor. Left unset, the empty
|
|
357
|
+
// target div collapses to 0px and the editor is invisible until it happens to be
|
|
358
|
+
// clicked - so hand the sizing to our own CSS instead (see the `.editor.inline` rule).
|
|
359
|
+
delete configurationOptions.height;
|
|
360
|
+
delete configurationOptions.width;
|
|
306
361
|
}
|
|
307
362
|
// set the maximum image size
|
|
308
363
|
const maxImageSize = this.configuration?.getValueByAlias('maxImageSize');
|
|
@@ -343,8 +398,15 @@ _UmbInputTinyMceElement_setTinyConfig = async function _UmbInputTinyMceElement_s
|
|
|
343
398
|
}
|
|
344
399
|
if (appSettingsConfig) {
|
|
345
400
|
if (appSettingsConfig.richTextEditor) {
|
|
346
|
-
|
|
347
|
-
|
|
401
|
+
// Plugins reach this section from two places: the dedicated `Plugins` setting, and a
|
|
402
|
+
// `plugins` entry inside `CustomConfig`. Both must be folded in before the assignment
|
|
403
|
+
// below, which overwrites whatever the deep merge produced. Miss the CustomConfig one
|
|
404
|
+
// and a plugin configured only there is silently dropped - the toolbar still lists its
|
|
405
|
+
// button, because the merged `toolbar` does survive, but TinyMCE never loaded the
|
|
406
|
+
// plugin so nothing renders for it.
|
|
407
|
+
const richTextEditorCustomConfig = parseJsonStringValues(appSettingsConfig.richTextEditor.customConfig);
|
|
408
|
+
const mergedPlugins = mergeArrays(mergeArrays(appSettingsConfig.richTextEditor.plugins, richTextEditorCustomConfig['plugins']), config.plugins);
|
|
409
|
+
config = umbDeepMerge(richTextEditorCustomConfig, config);
|
|
348
410
|
config.plugins = mergedPlugins;
|
|
349
411
|
if (appSettingsConfig.richTextEditor.validElements.length > 0) {
|
|
350
412
|
config.valid_elements = appSettingsConfig.richTextEditor.validElements;
|
|
@@ -446,6 +508,11 @@ _UmbInputTinyMceElement_editorSetup = async function _UmbInputTinyMceElement_edi
|
|
|
446
508
|
//console.log('#editorSetup end');
|
|
447
509
|
};
|
|
448
510
|
_UmbInputTinyMceElement_onInit = function _UmbInputTinyMceElement_onInit(editor) {
|
|
511
|
+
// Inline mode leaves the editable element in the backoffice's shadow DOM, which TinyMCE's
|
|
512
|
+
// document-scoped selection and id lookups cannot reach - see `shadow-dom-selection.ts`.
|
|
513
|
+
if (this._inline) {
|
|
514
|
+
bridgeInlineEditor(editor);
|
|
515
|
+
}
|
|
449
516
|
//enable browser based spell checking
|
|
450
517
|
editor.getBody().setAttribute('spellcheck', 'true');
|
|
451
518
|
uriAttributeSanitizer(editor);
|
|
@@ -470,6 +537,21 @@ UmbInputTinyMceElement.styles = [
|
|
|
470
537
|
position: absolute;
|
|
471
538
|
}
|
|
472
539
|
|
|
540
|
+
/* Inline mode edits this element directly - TinyMCE renders no chrome around it and
|
|
541
|
+
ignores the configured height, so it needs to look like a field on its own. */
|
|
542
|
+
.editor.inline {
|
|
543
|
+
box-sizing: border-box;
|
|
544
|
+
min-height: 100px;
|
|
545
|
+
padding: var(--uui-size-space-3, 9px);
|
|
546
|
+
background-color: var(--uui-color-surface, #fff);
|
|
547
|
+
border: var(--uui-input-border-width, 1px) solid var(--uui-input-border-color, var(--uui-color-border, #d8d7d9));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.editor.inline:focus {
|
|
551
|
+
outline: none;
|
|
552
|
+
border-color: var(--uui-color-focus, #3544b1);
|
|
553
|
+
}
|
|
554
|
+
|
|
473
555
|
/* FIXME: Remove this workaround when https://github.com/tinymce/tinymce/issues/6431 has been fixed */
|
|
474
556
|
.tox .tox-collection__item-label {
|
|
475
557
|
line-height: 1 !important;
|
|
@@ -485,6 +567,9 @@ __decorate([
|
|
|
485
567
|
__decorate([
|
|
486
568
|
query('.editor', true)
|
|
487
569
|
], UmbInputTinyMceElement.prototype, "_editorElement", void 0);
|
|
570
|
+
__decorate([
|
|
571
|
+
state()
|
|
572
|
+
], UmbInputTinyMceElement.prototype, "_inline", void 0);
|
|
488
573
|
UmbInputTinyMceElement = __decorate([
|
|
489
574
|
customElement('umb-input-tiny-mce')
|
|
490
575
|
], UmbInputTinyMceElement);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TinyMCE's inline mode assumes its editable element is somewhere document-level APIs can reach. In the
|
|
3
|
+
* backoffice it sits some twenty shadow roots deep (`umb-input-tiny-mce` -> ... -> `umb-app`), so three
|
|
4
|
+
* of those APIs silently fail and are bridged here. Classic mode needs none of it: its editable body is
|
|
5
|
+
* a plain document body inside the editor's own iframe.
|
|
6
|
+
*
|
|
7
|
+
* Every failure mode below is silent - no exception, no stack trace. See **Inline Mode and Shadow DOM**
|
|
8
|
+
* in this project's CLAUDE.md for the symptoms and how to test them.
|
|
9
|
+
*/
|
|
10
|
+
import type { Editor } from '@tiny-mce-umbraco/backoffice/external/tinymce';
|
|
11
|
+
/**
|
|
12
|
+
* Applies the shadow-DOM bridges to a freshly initialized inline editor. Call for inline editors only.
|
|
13
|
+
* @param editor The inline editor to bridge.
|
|
14
|
+
*/
|
|
15
|
+
export declare function bridgeInlineEditor(editor: Editor): void;
|
|
16
|
+
//# sourceMappingURL=shadow-dom-selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shadow-dom-selection.d.ts","sourceRoot":"","sources":["../../../src/components/input-tiny-mce/shadow-dom-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,+CAA+C,CAAC;AAwG5E;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,QAUhD"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TinyMCE's inline mode assumes its editable element is somewhere document-level APIs can reach. In the
|
|
3
|
+
* backoffice it sits some twenty shadow roots deep (`umb-input-tiny-mce` -> ... -> `umb-app`), so three
|
|
4
|
+
* of those APIs silently fail and are bridged here. Classic mode needs none of it: its editable body is
|
|
5
|
+
* a plain document body inside the editor's own iframe.
|
|
6
|
+
*
|
|
7
|
+
* Every failure mode below is silent - no exception, no stack trace. See **Inline Mode and Shadow DOM**
|
|
8
|
+
* in this project's CLAUDE.md for the symptoms and how to test them.
|
|
9
|
+
*/
|
|
10
|
+
/** Editable elements of the live inline editors; read by both document-level bridges. */
|
|
11
|
+
const inlineBodies = new Set();
|
|
12
|
+
let installed = false;
|
|
13
|
+
/**
|
|
14
|
+
* Resolved per call, never cached from focus/blur events: TinyMCE moves the selection while its own
|
|
15
|
+
* focus bookkeeping is mid-flight, and a stale flag there sends `setRng` to the wrong selection. This is
|
|
16
|
+
* the same liveness check TinyMCE's own `hasInlineFocus` makes.
|
|
17
|
+
*/
|
|
18
|
+
function getFocusedInlineBody() {
|
|
19
|
+
for (const body of inlineBodies) {
|
|
20
|
+
if (body.getRootNode().activeElement === body)
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* TinyMCE reads the caret through `win.getSelection()` (`getSel` in `Selection.getRng`), which in
|
|
27
|
+
* Chromium never descends into a shadow root; `ShadowRoot.getSelection()` does. Firefox is the reverse -
|
|
28
|
+
* see the note on the fallback below.
|
|
29
|
+
*
|
|
30
|
+
* Diverts only while one of our inline editors holds focus, so every other caller gets the native
|
|
31
|
+
* selection untouched.
|
|
32
|
+
*/
|
|
33
|
+
function installShadowDomSelectionBridge() {
|
|
34
|
+
const nativeGetSelection = window.getSelection.bind(window);
|
|
35
|
+
window.getSelection = function () {
|
|
36
|
+
const body = getFocusedInlineBody();
|
|
37
|
+
if (!body)
|
|
38
|
+
return nativeGetSelection();
|
|
39
|
+
// `ShadowRoot.getSelection()` is Chromium-only. The fallback is not a safety net - it is the
|
|
40
|
+
// working path in Firefox, whose `window.getSelection()` already pierces shadow roots. Removing
|
|
41
|
+
// it breaks Firefox. (Safari has neither and is unsupported.)
|
|
42
|
+
const selection = body.getRootNode().getSelection?.();
|
|
43
|
+
return selection ?? nativeGetSelection();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* `Selection.setRng` guards with `isValidRange` -> `isAttachedToDom` -> Sugar's `contains`, a plain
|
|
48
|
+
* `d1.contains(d2)` that does not cross shadow boundaries. Every range inside the editor is therefore
|
|
49
|
+
* judged detached, and `setRng` returns before it dispatches anything.
|
|
50
|
+
*
|
|
51
|
+
* Patched on the document object rather than `Node.prototype` because that is the exact call being
|
|
52
|
+
* made. Only ever turns `false` into `true`, and only for nodes inside a live inline editor.
|
|
53
|
+
*/
|
|
54
|
+
function installShadowDomAttachmentBridge() {
|
|
55
|
+
const nativeContains = document.contains.bind(document);
|
|
56
|
+
document.contains = (node) => {
|
|
57
|
+
if (nativeContains(node))
|
|
58
|
+
return true;
|
|
59
|
+
if (!node)
|
|
60
|
+
return false;
|
|
61
|
+
for (const body of inlineBodies) {
|
|
62
|
+
if (body === node || body.contains(node))
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Installs the document-level bridges, once per page. */
|
|
69
|
+
function installBridges() {
|
|
70
|
+
if (installed)
|
|
71
|
+
return;
|
|
72
|
+
installed = true;
|
|
73
|
+
installShadowDomSelectionBridge();
|
|
74
|
+
installShadowDomAttachmentBridge();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* `DOMUtils.get(id)` is `doc.getElementById(id)`, which cannot see into a shadow root. TinyMCE's
|
|
78
|
+
* bookmarks depend on it: operations that restructure the DOM drop `<span data-mce-type="bookmark">`
|
|
79
|
+
* markers and call `moveToBookmark` to restore the caret afterwards. Unresolvable markers mean the caret
|
|
80
|
+
* is never restored *and* the markers are never removed - they accumulate in the content and get saved.
|
|
81
|
+
*
|
|
82
|
+
* Searching the editor body first also scopes ids per editor rather than first-match-in-document.
|
|
83
|
+
* @param editor The inline editor to patch.
|
|
84
|
+
*/
|
|
85
|
+
function bridgeEditorLookups(editor) {
|
|
86
|
+
const dom = editor.dom;
|
|
87
|
+
const nativeGet = dom.get.bind(dom);
|
|
88
|
+
dom.get = (elm) => {
|
|
89
|
+
if (typeof elm !== 'string')
|
|
90
|
+
return nativeGet(elm);
|
|
91
|
+
const body = editor.getBody();
|
|
92
|
+
if (!body)
|
|
93
|
+
return nativeGet(elm);
|
|
94
|
+
// Inline mode's body is the target element itself, so it can be the id being asked for.
|
|
95
|
+
if (body.id === elm)
|
|
96
|
+
return body;
|
|
97
|
+
return body.querySelector(`#${CSS.escape(elm)}`) ?? nativeGet(elm);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Applies the shadow-DOM bridges to a freshly initialized inline editor. Call for inline editors only.
|
|
102
|
+
* @param editor The inline editor to bridge.
|
|
103
|
+
*/
|
|
104
|
+
export function bridgeInlineEditor(editor) {
|
|
105
|
+
const body = editor.getBody();
|
|
106
|
+
if (!body)
|
|
107
|
+
return;
|
|
108
|
+
bridgeEditorLookups(editor);
|
|
109
|
+
inlineBodies.add(body);
|
|
110
|
+
installBridges();
|
|
111
|
+
editor.on('remove', () => inlineBodies.delete(body));
|
|
112
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { UmbStylesheetRule } from '../../types.js';
|
|
2
2
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
|
3
|
-
declare const UmbStylesheetRuleInputElement_base: (new (...args: any[]) => import("@umbraco-ui/uui
|
|
3
|
+
declare const UmbStylesheetRuleInputElement_base: (new (...args: any[]) => import("@umbraco-ui/uui").UUIFormControlWithBasicsMixinElement<FormDataEntryValue | FormData>) & typeof UmbLitElement;
|
|
4
4
|
export declare class UmbStylesheetRuleInputElement extends UmbStylesheetRuleInputElement_base {
|
|
5
5
|
#private;
|
|
6
6
|
rules: UmbStylesheetRule[];
|