@tiny-mce-umbraco/backoffice 17.5.0 → 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 +103 -1
- 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/plugins/manifests.d.ts.map +1 -1
- package/dist-cms/plugins/manifests.js +17 -0
- package/dist-cms/plugins/tiny-mce-mediapicker.plugin.d.ts.map +1 -1
- package/dist-cms/plugins/tiny-mce-mediapicker.plugin.js +122 -42
- 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.js +1 -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 +4 -4
|
@@ -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;AAKjF,eAAO,MAAM,4CAA4C,sCAAsC,CAAC;AAEhG,eAAO,MAAM,yBAAyB,uQAmB5B,CAAC;AAMX,eAAO,MAAM,qBAAqB,EAAE,
|
|
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"}
|
|
@@ -59,6 +59,12 @@ export const defaultFallbackConfig = {
|
|
|
59
59
|
// });
|
|
60
60
|
//},
|
|
61
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;
|
|
62
68
|
// The following code is the context api proxy. [NL]
|
|
63
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]
|
|
64
70
|
editor.dom.doc.addEventListener(UMB_CONTEXT_REQUEST_EVENT_TYPE, ((event) => {
|
|
@@ -131,7 +137,25 @@ export const defaultFallbackConfig = {
|
|
|
131
137
|
// instance) from the outer document. Without this, requestDelete() in UmbBlockEntryContext
|
|
132
138
|
// falls back to returning raw keys because the iframe's manager has no registered translations.
|
|
133
139
|
const locSetsToSync = [];
|
|
134
|
-
|
|
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
|
+
];
|
|
135
159
|
umbLocalizationManager.localizations.forEach((locSet, code) => {
|
|
136
160
|
const locSetAny = locSet;
|
|
137
161
|
const entry = {
|
|
@@ -149,11 +173,42 @@ export const defaultFallbackConfig = {
|
|
|
149
173
|
if (hasKey)
|
|
150
174
|
locSetsToSync.push(entry);
|
|
151
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;
|
|
152
195
|
if (locSetsToSync.length > 0) {
|
|
153
196
|
const locScript = document.createElement('script');
|
|
154
197
|
locScript.setAttribute('type', 'module');
|
|
155
198
|
locScript.text = `
|
|
156
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.
|
|
157
212
|
${JSON.stringify(locSetsToSync)}.forEach(s => umbLocalizationManager.registerLocalization(s));
|
|
158
213
|
`;
|
|
159
214
|
editor.dom.doc.head.appendChild(locScript);
|
|
@@ -179,6 +234,13 @@ export const defaultFallbackConfig = {
|
|
|
179
234
|
import { umbExtensionsRegistry } from "@umbraco-cms/backoffice/extension-registry";
|
|
180
235
|
import { UMB_BLOCK_ACTION_DEFAULT_KIND_MANIFEST } from "@umbraco-cms/backoffice/block";
|
|
181
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";
|
|
182
244
|
|
|
183
245
|
// Register the blockAction default kind definition in the inner realm's registry.
|
|
184
246
|
// UMB_BLOCK_ACTION_DEFAULT_KIND_MANIFEST is imported here (inner realm), so its
|
|
@@ -195,6 +257,46 @@ export const defaultFallbackConfig = {
|
|
|
195
257
|
if (outerReg) {
|
|
196
258
|
const blockActions = outerReg.getByType('blockAction') ?? [];
|
|
197
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.
|
|
198
300
|
}
|
|
199
301
|
`;
|
|
200
302
|
editor.dom.doc.head.appendChild(script);
|
|
@@ -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[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workaround: let blob:/data: downloads through Umbraco's router.
|
|
3
|
+
*
|
|
4
|
+
* ## The problem
|
|
5
|
+
*
|
|
6
|
+
* Umbraco's router (`router-slot`) installs a global click listener that converts any same-origin
|
|
7
|
+
* anchor click into a history navigation — see
|
|
8
|
+
* `packages/core/router/router-slot/util/anchor.ts` (`ensureAnchorHistory`):
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* fullUrl = new URL(href, document.baseURI);
|
|
12
|
+
* if (fullUrl.origin !== location.origin || ...) return;
|
|
13
|
+
* e.preventDefault();
|
|
14
|
+
* history.pushState(null, '', fullUrl);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Blob URLs inherit the origin of the document that created them, so
|
|
18
|
+
* `new URL('blob:https://site:44308/<uuid>').origin` is `https://site:44308` — the origin guard
|
|
19
|
+
* passes and the anchor is treated as an internal navigation. The router then:
|
|
20
|
+
*
|
|
21
|
+
* 1. calls `e.preventDefault()`, which **cancels the download**, and
|
|
22
|
+
* 2. calls `history.pushState` with a `blob:` URL, which is invalid and throws
|
|
23
|
+
* `SecurityError: A history state object with URL 'blob:…' cannot be created in a document
|
|
24
|
+
* with origin '…'`.
|
|
25
|
+
*
|
|
26
|
+
* The net effect is that any download served from a `blob:` or `data:` URL inside the backoffice
|
|
27
|
+
* silently fails. TinyMCE's `export` plugin (PDF export via `clientpdf`) hits this on every
|
|
28
|
+
* download attempt.
|
|
29
|
+
*
|
|
30
|
+
* ## The fix
|
|
31
|
+
*
|
|
32
|
+
* The router deliberately skips anchors carrying `data-router-slot="disabled"`. We cannot set that
|
|
33
|
+
* at creation time because the anchor is created inside TinyMCE's minified plugin bundle, so we tag
|
|
34
|
+
* it in flight instead: this listener is registered in the **capture** phase on `window`, which runs
|
|
35
|
+
* before the router's own bubble-phase listener on that same target. By the time the router sees the
|
|
36
|
+
* event, the anchor is marked and the router leaves it alone.
|
|
37
|
+
*
|
|
38
|
+
* Only `blob:` and `data:` are tagged. Ordinary links are untouched, so in-app navigation is
|
|
39
|
+
* unaffected.
|
|
40
|
+
*
|
|
41
|
+
* ## Upstream
|
|
42
|
+
*
|
|
43
|
+
* This is arguably an Umbraco defect: the router should not attempt to route non-`http(s)` schemes
|
|
44
|
+
* at all, which would also cover `mailto:` and `tel:`. It affects any blob-based download in the
|
|
45
|
+
* backoffice, not only TinyMCE. Verified present in Umbraco 17.6.2, and `anchor.ts` is byte-identical
|
|
46
|
+
* back to at least 17.5.0 — so this is long-standing, not a regression. Remove this workaround once
|
|
47
|
+
* the router gains a scheme guard.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Starts tagging `blob:`/`data:` anchors so Umbraco's router does not cancel the download.
|
|
51
|
+
* Safe to call more than once; only the first call registers a listener.
|
|
52
|
+
*/
|
|
53
|
+
export declare function enableBlobDownloadWorkaround(): void;
|
|
54
|
+
/** Removes the listener registered by {@link enableBlobDownloadWorkaround}. */
|
|
55
|
+
export declare function disableBlobDownloadWorkaround(): void;
|
|
56
|
+
//# sourceMappingURL=blob-download-router-fix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blob-download-router-fix.d.ts","sourceRoot":"","sources":["../../src/entry-points/blob-download-router-fix.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAqBH;;;GAGG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAKnD;AAED,+EAA+E;AAC/E,wBAAgB,6BAA6B,IAAI,IAAI,CAIpD"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workaround: let blob:/data: downloads through Umbraco's router.
|
|
3
|
+
*
|
|
4
|
+
* ## The problem
|
|
5
|
+
*
|
|
6
|
+
* Umbraco's router (`router-slot`) installs a global click listener that converts any same-origin
|
|
7
|
+
* anchor click into a history navigation — see
|
|
8
|
+
* `packages/core/router/router-slot/util/anchor.ts` (`ensureAnchorHistory`):
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* fullUrl = new URL(href, document.baseURI);
|
|
12
|
+
* if (fullUrl.origin !== location.origin || ...) return;
|
|
13
|
+
* e.preventDefault();
|
|
14
|
+
* history.pushState(null, '', fullUrl);
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Blob URLs inherit the origin of the document that created them, so
|
|
18
|
+
* `new URL('blob:https://site:44308/<uuid>').origin` is `https://site:44308` — the origin guard
|
|
19
|
+
* passes and the anchor is treated as an internal navigation. The router then:
|
|
20
|
+
*
|
|
21
|
+
* 1. calls `e.preventDefault()`, which **cancels the download**, and
|
|
22
|
+
* 2. calls `history.pushState` with a `blob:` URL, which is invalid and throws
|
|
23
|
+
* `SecurityError: A history state object with URL 'blob:…' cannot be created in a document
|
|
24
|
+
* with origin '…'`.
|
|
25
|
+
*
|
|
26
|
+
* The net effect is that any download served from a `blob:` or `data:` URL inside the backoffice
|
|
27
|
+
* silently fails. TinyMCE's `export` plugin (PDF export via `clientpdf`) hits this on every
|
|
28
|
+
* download attempt.
|
|
29
|
+
*
|
|
30
|
+
* ## The fix
|
|
31
|
+
*
|
|
32
|
+
* The router deliberately skips anchors carrying `data-router-slot="disabled"`. We cannot set that
|
|
33
|
+
* at creation time because the anchor is created inside TinyMCE's minified plugin bundle, so we tag
|
|
34
|
+
* it in flight instead: this listener is registered in the **capture** phase on `window`, which runs
|
|
35
|
+
* before the router's own bubble-phase listener on that same target. By the time the router sees the
|
|
36
|
+
* event, the anchor is marked and the router leaves it alone.
|
|
37
|
+
*
|
|
38
|
+
* Only `blob:` and `data:` are tagged. Ordinary links are untouched, so in-app navigation is
|
|
39
|
+
* unaffected.
|
|
40
|
+
*
|
|
41
|
+
* ## Upstream
|
|
42
|
+
*
|
|
43
|
+
* This is arguably an Umbraco defect: the router should not attempt to route non-`http(s)` schemes
|
|
44
|
+
* at all, which would also cover `mailto:` and `tel:`. It affects any blob-based download in the
|
|
45
|
+
* backoffice, not only TinyMCE. Verified present in Umbraco 17.6.2, and `anchor.ts` is byte-identical
|
|
46
|
+
* back to at least 17.5.0 — so this is long-standing, not a regression. Remove this workaround once
|
|
47
|
+
* the router gains a scheme guard.
|
|
48
|
+
*/
|
|
49
|
+
/** Schemes that must never be handed to `history.pushState`. */
|
|
50
|
+
const NON_ROUTABLE_SCHEME = /^(?:blob|data):/i;
|
|
51
|
+
let isListening = false;
|
|
52
|
+
const onClickCapture = (event) => {
|
|
53
|
+
// composedPath() is required to see through shadow boundaries — the same approach the
|
|
54
|
+
// router itself uses.
|
|
55
|
+
const anchor = event
|
|
56
|
+
.composedPath()
|
|
57
|
+
.find((target) => target instanceof HTMLAnchorElement);
|
|
58
|
+
if (!anchor || !NON_ROUTABLE_SCHEME.test(anchor.href))
|
|
59
|
+
return;
|
|
60
|
+
// Opt this anchor out of router-slot handling. The router reads
|
|
61
|
+
// `$anchor.dataset['routerSlot'] === 'disabled'` and returns early.
|
|
62
|
+
anchor.dataset['routerSlot'] = 'disabled';
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Starts tagging `blob:`/`data:` anchors so Umbraco's router does not cancel the download.
|
|
66
|
+
* Safe to call more than once; only the first call registers a listener.
|
|
67
|
+
*/
|
|
68
|
+
export function enableBlobDownloadWorkaround() {
|
|
69
|
+
if (isListening)
|
|
70
|
+
return;
|
|
71
|
+
// Capture phase, so this runs before router-slot's bubble-phase listener on `window`.
|
|
72
|
+
window.addEventListener('click', onClickCapture, true);
|
|
73
|
+
isListening = true;
|
|
74
|
+
}
|
|
75
|
+
/** Removes the listener registered by {@link enableBlobDownloadWorkaround}. */
|
|
76
|
+
export function disableBlobDownloadWorkaround() {
|
|
77
|
+
if (!isListening)
|
|
78
|
+
return;
|
|
79
|
+
window.removeEventListener('click', onClickCapture, true);
|
|
80
|
+
isListening = false;
|
|
81
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-point.d.ts","sourceRoot":"","sources":["../../src/entry-points/entry-point.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entry-point.d.ts","sourceRoot":"","sources":["../../src/entry-points/entry-point.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAExG,eAAO,MAAM,MAAM,EAAE,mBA2BpB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,qBAEtB,CAAC"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { client } from '../api/client.gen.js';
|
|
2
|
+
import { disableBlobDownloadWorkaround, enableBlobDownloadWorkaround, } from './blob-download-router-fix.js';
|
|
2
3
|
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
|
|
3
4
|
export const onInit = (_host, _extensionRegistry) => {
|
|
5
|
+
// Let blob:/data: downloads (e.g. TinyMCE's PDF export) through Umbraco's router.
|
|
6
|
+
// See blob-download-router-fix.ts for why this is needed.
|
|
7
|
+
enableBlobDownloadWorkaround();
|
|
4
8
|
// Will use only to add in Open API config with generated TS OpenAPI HTTPS Client
|
|
5
9
|
// Do the OAuth token handshake stuff
|
|
6
10
|
_host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => {
|
|
@@ -22,4 +26,6 @@ export const onInit = (_host, _extensionRegistry) => {
|
|
|
22
26
|
});
|
|
23
27
|
});
|
|
24
28
|
};
|
|
25
|
-
export const onUnload = (_host, _extensionRegistry) => {
|
|
29
|
+
export const onUnload = (_host, _extensionRegistry) => {
|
|
30
|
+
disableBlobDownloadWorkaround();
|
|
31
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifests.d.ts","sourceRoot":"","sources":["../../src/plugins/manifests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE/E,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,qBAAqB,
|
|
1
|
+
{"version":3,"file":"manifests.d.ts","sourceRoot":"","sources":["../../src/plugins/manifests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAE/E,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,qBAAqB,CA2gBlD,CAAC"}
|
|
@@ -136,6 +136,23 @@ export const manifests = [
|
|
|
136
136
|
],
|
|
137
137
|
},
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
type: 'tinyMcePlugin',
|
|
141
|
+
alias: 'Umb.TinyMcePlugin.Fullscreen',
|
|
142
|
+
name: 'TinyMCE Fullscreen Plugin',
|
|
143
|
+
meta: {
|
|
144
|
+
plugins: ['fullscreen'],
|
|
145
|
+
toolbar: [
|
|
146
|
+
{
|
|
147
|
+
alias: 'fullscreen',
|
|
148
|
+
label: 'Fullscreen',
|
|
149
|
+
icon: 'fullscreen',
|
|
150
|
+
isplugin: true,
|
|
151
|
+
pluginAlias: 'fullscreen',
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
139
156
|
{
|
|
140
157
|
type: 'tinyMcePlugin',
|
|
141
158
|
alias: 'Umb.TinyMcePlugin.Help',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tiny-mce-mediapicker.plugin.d.ts","sourceRoot":"","sources":["../../src/plugins/tiny-mce-mediapicker.plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"tiny-mce-mediapicker.plugin.d.ts","sourceRoot":"","sources":["../../src/plugins/tiny-mce-mediapicker.plugin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAyChF,MAAM,CAAC,OAAO,OAAO,2BAA4B,SAAQ,oBAAoB;;gBAQhE,IAAI,EAAE,sBAAsB;CA2VxC"}
|
|
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var _UmbTinyMceMediaPickerPlugin_instances, _UmbTinyMceMediaPickerPlugin_modalManager, _UmbTinyMceMediaPickerPlugin_temporaryFileRepository, _UmbTinyMceMediaPickerPlugin_allowedMediaTypeIds_get, _UmbTinyMceMediaPickerPlugin_onAction, _UmbTinyMceMediaPickerPlugin_showMediaPicker, _UmbTinyMceMediaPickerPlugin_showMediaCaptionAltText, _UmbTinyMceMediaPickerPlugin_insertInEditor, _UmbTinyMceMediaPickerPlugin_uploadImageHandler;
|
|
12
|
+
var _UmbTinyMceMediaPickerPlugin_instances, _UmbTinyMceMediaPickerPlugin_modalManager, _UmbTinyMceMediaPickerPlugin_temporaryFileRepository, _UmbTinyMceMediaPickerPlugin_allowedMediaTypeIds_get, _UmbTinyMceMediaPickerPlugin_resolveExistingMedia, _UmbTinyMceMediaPickerPlugin_onAction, _UmbTinyMceMediaPickerPlugin_parseDimension, _UmbTinyMceMediaPickerPlugin_showMediaPicker, _UmbTinyMceMediaPickerPlugin_showMediaCaptionAltText, _UmbTinyMceMediaPickerPlugin_insertInEditor, _UmbTinyMceMediaPickerPlugin_uploadImageHandler;
|
|
13
13
|
import { sizeImageInEditor, uploadBlobImages } from '@tiny-mce-umbraco/backoffice/core';
|
|
14
14
|
import { UmbTinyMcePluginBase } from '@tiny-mce-umbraco/backoffice/core';
|
|
15
15
|
import { getGuidFromUdi, splitStringToArray } from '@umbraco-cms/backoffice/utils';
|
|
@@ -62,8 +62,14 @@ class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
|
|
|
62
62
|
tooltip: localize.term('general_mediaPicker'),
|
|
63
63
|
onAction: () => __classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_onAction).call(this),
|
|
64
64
|
onSetup: (api) => {
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// Drive the active state from the SAME resolution the action uses. A selector of
|
|
66
|
+
// `img[data-udi]` only lights up when the image itself is selected, so with the caret in
|
|
67
|
+
// the figcaption the button looked like "nothing selected" while the action would happily
|
|
68
|
+
// have edited that image. Button state and action behaviour must agree.
|
|
69
|
+
const syncActiveState = () => api.setActive(!!__classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_resolveExistingMedia).call(this).image);
|
|
70
|
+
this.editor.on('NodeChange', syncActiveState);
|
|
71
|
+
syncActiveState();
|
|
72
|
+
return () => this.editor.off('NodeChange', syncActiveState);
|
|
67
73
|
},
|
|
68
74
|
});
|
|
69
75
|
// Register global options for the editor
|
|
@@ -88,37 +94,49 @@ class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
|
|
|
88
94
|
}
|
|
89
95
|
_UmbTinyMceMediaPickerPlugin_modalManager = new WeakMap(), _UmbTinyMceMediaPickerPlugin_temporaryFileRepository = new WeakMap(), _UmbTinyMceMediaPickerPlugin_uploadImageHandler = new WeakMap(), _UmbTinyMceMediaPickerPlugin_instances = new WeakSet(), _UmbTinyMceMediaPickerPlugin_allowedMediaTypeIds_get = function _UmbTinyMceMediaPickerPlugin_allowedMediaTypeIds_get() {
|
|
90
96
|
return splitStringToArray(this.configuration?.getValueByAlias('allowedMediaTypes'));
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
}, _UmbTinyMceMediaPickerPlugin_resolveExistingMedia = function _UmbTinyMceMediaPickerPlugin_resolveExistingMedia() {
|
|
98
|
+
const dom = this.editor.dom;
|
|
99
|
+
const selectedNode = this.editor.selection.getNode();
|
|
100
|
+
const figure = dom.getParent(selectedNode, 'figure');
|
|
101
|
+
const image = selectedNode.nodeName === 'IMG'
|
|
102
|
+
? selectedNode
|
|
103
|
+
: (figure?.querySelector('img') ?? null);
|
|
104
|
+
const anchor = image ? dom.getParent(image, 'a') : null;
|
|
105
|
+
// The `<figcaption>` is what the editor actually sees and edits, so it wins over the
|
|
106
|
+
// `data-caption` attribute, which is only written when the picker last ran and goes stale as
|
|
107
|
+
// soon as someone edits the caption directly in the editor.
|
|
108
|
+
const figcaptionText = figure?.querySelector('figcaption')?.textContent?.trim();
|
|
109
|
+
const caption = figcaptionText || image?.dataset.caption || undefined;
|
|
110
|
+
return { selectedNode, figure, image, anchor, caption };
|
|
111
|
+
}, _UmbTinyMceMediaPickerPlugin_onAction = async function _UmbTinyMceMediaPickerPlugin_onAction() {
|
|
112
|
+
const { image, caption } = __classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_resolveExistingMedia).call(this);
|
|
101
113
|
let currentTarget = {};
|
|
102
|
-
if (
|
|
103
|
-
const img = selectedElm;
|
|
104
|
-
const hasUdi = img.hasAttribute('data-udi');
|
|
105
|
-
const hasDataTmpImg = img.hasAttribute('data-tmpimg');
|
|
114
|
+
if (image) {
|
|
106
115
|
currentTarget = {
|
|
107
|
-
altText:
|
|
108
|
-
url:
|
|
109
|
-
caption
|
|
116
|
+
altText: image.alt,
|
|
117
|
+
url: image.src,
|
|
118
|
+
caption,
|
|
119
|
+
// Carry the current dimensions in so the dialog opens showing the real size rather
|
|
120
|
+
// than defaulting, and so an unchanged dialog round-trips the size unchanged.
|
|
121
|
+
width: __classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_parseDimension).call(this, image.getAttribute('width')),
|
|
122
|
+
height: __classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_parseDimension).call(this, image.getAttribute('height')),
|
|
110
123
|
};
|
|
111
|
-
if (
|
|
112
|
-
currentTarget['udi'] =
|
|
124
|
+
if (image.hasAttribute('data-udi')) {
|
|
125
|
+
currentTarget['udi'] = image.dataset.udi;
|
|
113
126
|
}
|
|
114
127
|
else {
|
|
115
|
-
currentTarget['id'] =
|
|
128
|
+
currentTarget['id'] = image.getAttribute('rel') ?? undefined;
|
|
116
129
|
}
|
|
117
|
-
if (
|
|
118
|
-
currentTarget['tmpimg'] =
|
|
130
|
+
if (image.hasAttribute('data-tmpimg')) {
|
|
131
|
+
currentTarget['tmpimg'] = image.dataset.tmpimg;
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
__classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_showMediaPicker).call(this, currentTarget);
|
|
135
|
+
}, _UmbTinyMceMediaPickerPlugin_parseDimension = function _UmbTinyMceMediaPickerPlugin_parseDimension(value) {
|
|
136
|
+
if (!value)
|
|
137
|
+
return undefined;
|
|
138
|
+
const parsed = Number.parseInt(value, 10);
|
|
139
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
122
140
|
}, _UmbTinyMceMediaPickerPlugin_showMediaPicker = async function _UmbTinyMceMediaPickerPlugin_showMediaPicker(currentTarget) {
|
|
123
141
|
/*
|
|
124
142
|
let startNodeId;
|
|
@@ -156,12 +174,20 @@ async function _UmbTinyMceMediaPickerPlugin_onAction() {
|
|
|
156
174
|
if (!mediaUnique)
|
|
157
175
|
return;
|
|
158
176
|
const modalHandler = __classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_modalManager, "f")?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, {
|
|
159
|
-
|
|
177
|
+
// The dialog needs maxImageSize to offer sensible sizes; without it the size it hands back
|
|
178
|
+
// is unconstrained and we have no idea what the editor's limit was meant to be.
|
|
179
|
+
data: { mediaUnique, maxImageSize: this.editor.options.get('maxImageSize') },
|
|
160
180
|
value: {
|
|
161
181
|
url: '',
|
|
162
182
|
altText: currentTarget.altText,
|
|
163
183
|
caption: currentTarget.caption,
|
|
184
|
+
width: currentTarget.width,
|
|
185
|
+
height: currentTarget.height,
|
|
164
186
|
},
|
|
187
|
+
// The token defaults this sidebar to `small`, which is too narrow for the width/height row —
|
|
188
|
+
// the unit suffix on the height field gets clipped. Umbraco's own tiptap RTE overrides it the
|
|
189
|
+
// same way for the same reason ("Override default sidebar size for better UX").
|
|
190
|
+
modal: { size: 'medium' },
|
|
165
191
|
});
|
|
166
192
|
const mediaData = await modalHandler?.onSubmit().catch(() => null);
|
|
167
193
|
if (!mediaData)
|
|
@@ -171,6 +197,9 @@ async function _UmbTinyMceMediaPickerPlugin_onAction() {
|
|
|
171
197
|
caption: mediaData?.caption,
|
|
172
198
|
url: mediaData?.url,
|
|
173
199
|
udi: 'umb://media/' + mediaUnique?.replace(/-/g, ''),
|
|
200
|
+
// The dialog is the authority on size once the editor has chosen one.
|
|
201
|
+
width: mediaData?.width,
|
|
202
|
+
height: mediaData?.height,
|
|
174
203
|
};
|
|
175
204
|
__classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_insertInEditor).call(this, media);
|
|
176
205
|
}, _UmbTinyMceMediaPickerPlugin_insertInEditor = async function _UmbTinyMceMediaPickerPlugin_insertInEditor(media) {
|
|
@@ -178,32 +207,78 @@ async function _UmbTinyMceMediaPickerPlugin_onAction() {
|
|
|
178
207
|
return;
|
|
179
208
|
// We need to create a NEW DOM <img> element to insert
|
|
180
209
|
// setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image.
|
|
210
|
+
// An explicit size from the dialog is authoritative. Stamping it on the markup up front means
|
|
211
|
+
// the editor never briefly renders at the wrong size, and it tells the post-insert sizing step
|
|
212
|
+
// below to leave the dimensions alone.
|
|
213
|
+
const explicitSize = media.width && media.height ? { width: media.width, height: media.height } : undefined;
|
|
181
214
|
const img = {
|
|
182
215
|
alt: media.altText,
|
|
183
216
|
src: media.url ? media.url : 'nothing.jpg',
|
|
184
217
|
id: '__mcenew',
|
|
185
218
|
'data-udi': media.udi,
|
|
186
219
|
'data-caption': media.caption,
|
|
220
|
+
...(explicitSize
|
|
221
|
+
? { width: String(explicitSize.width), height: String(explicitSize.height) }
|
|
222
|
+
: {}),
|
|
187
223
|
};
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
224
|
+
const dom = this.editor.dom;
|
|
225
|
+
// Same resolution the edit path used, so both agree on what is being replaced. The caret may
|
|
226
|
+
// be on the image, on an <a> wrapping it, or inside the figcaption.
|
|
227
|
+
//
|
|
228
|
+
// The anchor matters because Umbraco fixed this same class of bug in its own RTE in 17.6
|
|
229
|
+
// (media-picker.tiptap-toolbar.utils.ts); re-picking used to silently drop the <a>.
|
|
230
|
+
const { selectedNode, figure: existingFigure, image: existingImage, anchor: existingAnchor, } = __classPrivateFieldGet(this, _UmbTinyMceMediaPickerPlugin_instances, "m", _UmbTinyMceMediaPickerPlugin_resolveExistingMedia).call(this);
|
|
231
|
+
let markup = dom.createHTML('img', img);
|
|
232
|
+
if (existingAnchor) {
|
|
233
|
+
// Rebuild the wrapping link. `data-mce-*` attributes are TinyMCE's own bookkeeping and are
|
|
234
|
+
// skipped so they get regenerated rather than staling.
|
|
235
|
+
//
|
|
236
|
+
// Because the new markup now CONTAINS the link, whatever we replace below must be the
|
|
237
|
+
// element the old link lived in — never the image inside it. Replacing just the image would
|
|
238
|
+
// nest an <a> inside an <a>; the parser rejects that and splits it, leaving an empty
|
|
239
|
+
// duplicate link behind.
|
|
240
|
+
const anchorAttrs = {};
|
|
241
|
+
for (const attr of Array.from(existingAnchor.attributes)) {
|
|
242
|
+
if (!attr.name.startsWith('data-mce-')) {
|
|
243
|
+
anchorAttrs[attr.name] = attr.value;
|
|
244
|
+
}
|
|
199
245
|
}
|
|
246
|
+
markup = dom.createHTML('a', anchorAttrs, markup);
|
|
200
247
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
const caption = img['data-caption'];
|
|
249
|
+
if (caption && existingFigure) {
|
|
250
|
+
// Replace only the figure's children, so the figure's own attributes survive along with
|
|
251
|
+
// every sibling elsewhere in the container.
|
|
252
|
+
existingFigure.innerHTML = markup + dom.createHTML('figcaption', {}, caption);
|
|
204
253
|
}
|
|
205
254
|
else {
|
|
206
|
-
|
|
255
|
+
const content = caption ? dom.createHTML('figure', {}, markup + dom.createHTML('figcaption', {}, caption)) : markup;
|
|
256
|
+
// Replace the OUTERMOST element whose markup we have just rebuilt: the figure if there was
|
|
257
|
+
// one, otherwise the link, otherwise the image. Anything narrower would leave a fragment of
|
|
258
|
+
// the old structure behind; anything wider would take out unrelated siblings — the bug in
|
|
259
|
+
// the previous implementation, which assigned to `figure.parentElement.innerHTML`.
|
|
260
|
+
let replaceTarget = existingFigure ?? existingAnchor ?? existingImage;
|
|
261
|
+
if (caption && !existingFigure) {
|
|
262
|
+
// A <figure> is block-level and cannot legally sit inside a <p>. Replacing something
|
|
263
|
+
// *within* the paragraph makes the parser split it, leaving an empty <p> either side of
|
|
264
|
+
// the figure. So when the paragraph holds nothing but this image, promote the target to
|
|
265
|
+
// the paragraph and consume it.
|
|
266
|
+
//
|
|
267
|
+
// Guarded on the paragraph having no text of its own and at most one image, so a figure
|
|
268
|
+
// never swallows a paragraph that has other content in it. `length <= 1` rather than
|
|
269
|
+
// `=== 1` because a brand new insert at a caret has no image in the paragraph yet.
|
|
270
|
+
const paragraph = dom.getParent(replaceTarget ?? selectedNode, 'p');
|
|
271
|
+
if (paragraph && !paragraph.textContent?.trim() && paragraph.querySelectorAll('img').length <= 1) {
|
|
272
|
+
replaceTarget = paragraph;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (replaceTarget) {
|
|
276
|
+
dom.setOuterHTML(replaceTarget, content);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Nothing existing to replace: a brand new insert at the caret.
|
|
280
|
+
this.editor.selection.setContent(content);
|
|
281
|
+
}
|
|
207
282
|
}
|
|
208
283
|
// Using settimeout to wait for a DoM-render, so we can find the new element by ID.
|
|
209
284
|
setTimeout(() => {
|
|
@@ -212,8 +287,13 @@ async function _UmbTinyMceMediaPickerPlugin_onAction() {
|
|
|
212
287
|
return;
|
|
213
288
|
this.editor.dom.setAttrib(imgElm, 'id', null);
|
|
214
289
|
// When image is loaded we are ready to call sizeImageInEditor.
|
|
290
|
+
//
|
|
291
|
+
// `sizeImageInEditor` reads the image's natural size and scales it down to maxImageSize,
|
|
292
|
+
// overwriting the width/height attributes. That is right for pasted or dragged images, but
|
|
293
|
+
// it used to silently discard whatever size the editor chose in the dialog. Passing the
|
|
294
|
+
// explicit size keeps it — the sizer then only refreshes the processed image URL.
|
|
215
295
|
const onImageLoaded = () => {
|
|
216
|
-
sizeImageInEditor(this.editor, imgElm, img.src);
|
|
296
|
+
sizeImageInEditor(this.editor, imgElm, img.src, explicitSize);
|
|
217
297
|
this.editor.dispatch('Change');
|
|
218
298
|
};
|
|
219
299
|
// Check if image already is loaded.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"property-editor-ui-tiny-mce-config-editor.element.d.ts","sourceRoot":"","sources":["../../../src/property-editors/configEditor/property-editor-ui-tiny-mce-config-editor.element.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EACX,0BAA0B,EAC1B,iCAAiC,EACjC,MAAM,yCAAyC,CAAC;AAKjD;;GAEG;AACH,qBACa,6CAA8C,SAAQ,aAAc,YAAW,0BAA0B;;IAErH,IACI,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,EAI3B;IACD,IAAI,KAAK,IAAI,IAAI,CAEhB;IAGD,MAAM,CAAC,EAAE,iCAAiC,CAAC;IAG3C,OAAO,CAAC,aAAa,CAA0B;cAEtB,YAAY,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"property-editor-ui-tiny-mce-config-editor.element.d.ts","sourceRoot":"","sources":["../../../src/property-editors/configEditor/property-editor-ui-tiny-mce-config-editor.element.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EACX,0BAA0B,EAC1B,iCAAiC,EACjC,MAAM,yCAAyC,CAAC;AAKjD;;GAEG;AACH,qBACa,6CAA8C,SAAQ,aAAc,YAAW,0BAA0B;;IAErH,IACI,KAAK,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI,EAI3B;IACD,IAAI,KAAK,IAAI,IAAI,CAEhB;IAGD,MAAM,CAAC,EAAE,iCAAiC,CAAC;IAG3C,OAAO,CAAC,aAAa,CAA0B;cAEtB,YAAY,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC;IAoC1E,MAAM;IASf,gBAAyB,MAAM,4BAY7B;CACF;AAED,eAAe,6CAA6C,CAAC;AAE7D,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,qBAAqB;QAC9B,+CAA+C,EAAE,6CAA6C,CAAC;KAC/F;CACD"}
|
package/dist-cms/property-editors/configEditor/property-editor-ui-tiny-mce-config-editor.element.js
CHANGED
|
@@ -64,7 +64,6 @@ _UmbPropertyEditorUITinyMceConfigEditorElement_showCodeEditor = async function _
|
|
|
64
64
|
else {
|
|
65
65
|
try {
|
|
66
66
|
this._customConfig = JSON.parse(value.content.toString());
|
|
67
|
-
console.log('Parsed object:', this._customConfig);
|
|
68
67
|
// Proceed with using jsonObj
|
|
69
68
|
}
|
|
70
69
|
catch (err) {
|
|
@@ -9,7 +9,7 @@ export const manifests = [
|
|
|
9
9
|
meta: {
|
|
10
10
|
label: 'TinyMCE Plugin Configuration',
|
|
11
11
|
icon: 'icon-autofill',
|
|
12
|
-
group: '
|
|
12
|
+
group: '#propertyEditorUIGroups_common',
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
{
|
|
@@ -20,7 +20,7 @@ export const manifests = [
|
|
|
20
20
|
meta: {
|
|
21
21
|
label: 'TinyMCE Toolbar Configuration',
|
|
22
22
|
icon: 'icon-autofill',
|
|
23
|
-
group: '
|
|
23
|
+
group: '#propertyEditorUIGroups_common',
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
{
|
|
@@ -31,7 +31,7 @@ export const manifests = [
|
|
|
31
31
|
meta: {
|
|
32
32
|
label: 'TinyMCE Custom Configuration',
|
|
33
33
|
icon: 'icon-autofill',
|
|
34
|
-
group: '
|
|
34
|
+
group: '#propertyEditorUIGroups_common',
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"property-editor-ui-tiny-mce-plugin-configuration.element.d.ts","sourceRoot":"","sources":["../../../src/property-editors/plugin/property-editor-ui-tiny-mce-plugin-configuration.element.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EACX,0BAA0B,EAC1B,iCAAiC,EACjC,MAAM,yCAAyC,CAAC;AAejD;;GAEG;AACH,qBACa,oDACZ,SAAQ,aACR,YAAW,0BAA0B;;IAErC,IACI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EAexC;IACD,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAGD,MAAM,CAAC,EAAE,iCAAiC,CAAC;IAG3C,OAAO,CAAC,aAAa,CAAsB;cAIlB,YAAY,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC;YAuBrE,UAAU;
|
|
1
|
+
{"version":3,"file":"property-editor-ui-tiny-mce-plugin-configuration.element.d.ts","sourceRoot":"","sources":["../../../src/property-editors/plugin/property-editor-ui-tiny-mce-plugin-configuration.element.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,KAAK,EACX,0BAA0B,EAC1B,iCAAiC,EACjC,MAAM,yCAAyC,CAAC;AAejD;;GAEG;AACH,qBACa,oDACZ,SAAQ,aACR,YAAW,0BAA0B;;IAErC,IACI,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,EAexC;IACD,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAGD,MAAM,CAAC,EAAE,iCAAiC,CAAC;IAG3C,OAAO,CAAC,aAAa,CAAsB;cAIlB,YAAY,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,OAAO,CAAC;YAuBrE,UAAU;IAqGxB,OAAO,CAAC,QAAQ;IAaP,MAAM;IAoBf,gBAAyB,MAAM,4BAS7B;CACF;AAED,eAAe,oDAAoD,CAAC;AAEpE,OAAO,CAAC,MAAM,CAAC;IACd,UAAU,qBAAqB;QAC9B,sDAAsD,EAAE,oDAAoD,CAAC;KAC7G;CACD"}
|
package/dist-cms/property-editors/plugin/property-editor-ui-tiny-mce-plugin-configuration.element.js
CHANGED
|
@@ -114,10 +114,20 @@ let UmbPropertyEditorUITinyMcePluginConfigurationElement = class UmbPropertyEdit
|
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
// A plugin can be named by more than one source - the fallback defaults, the appsettings
|
|
118
|
+
// `Plugins` list, and a `tinyMcePlugin` manifest. Each source used to push its own row, so a
|
|
119
|
+
// plugin named twice appeared twice in the picker and handed lit's keyed `repeat()` a duplicate
|
|
120
|
+
// key. First source wins, which keeps the defaults' "(default plugin)" label and disabled state.
|
|
121
|
+
const seen = new Set(this._pluginConfig.map((p) => p.alias));
|
|
122
|
+
const addPlugin = (entry) => {
|
|
123
|
+
if (seen.has(entry.alias))
|
|
124
|
+
return;
|
|
125
|
+
seen.add(entry.alias);
|
|
126
|
+
this._pluginConfig.push(entry);
|
|
127
|
+
};
|
|
118
128
|
configPlugins.forEach((p) => {
|
|
119
|
-
if (!excludeList.includes(p)
|
|
120
|
-
|
|
129
|
+
if (!excludeList.includes(p)) {
|
|
130
|
+
addPlugin({
|
|
121
131
|
alias: p,
|
|
122
132
|
label: p,
|
|
123
133
|
icon: undefined,
|
|
@@ -129,7 +139,7 @@ let UmbPropertyEditorUITinyMcePluginConfigurationElement = class UmbPropertyEdit
|
|
|
129
139
|
plugins.forEach((p) => {
|
|
130
140
|
if (p.meta?.plugins) {
|
|
131
141
|
if (typeof p.meta.plugins === 'string' && !excludeList.includes(p.meta.plugins)) {
|
|
132
|
-
|
|
142
|
+
addPlugin({
|
|
133
143
|
alias: p.meta.plugins,
|
|
134
144
|
label: p.meta.plugins,
|
|
135
145
|
icon: undefined,
|
|
@@ -140,7 +150,7 @@ let UmbPropertyEditorUITinyMcePluginConfigurationElement = class UmbPropertyEdit
|
|
|
140
150
|
else if (Array.isArray(p.meta.plugins)) {
|
|
141
151
|
p.meta.plugins.forEach((pl) => {
|
|
142
152
|
if (!excludeList.includes(pl)) {
|
|
143
|
-
|
|
153
|
+
addPlugin({
|
|
144
154
|
alias: pl,
|
|
145
155
|
label: pl,
|
|
146
156
|
icon: undefined,
|
|
@@ -17,7 +17,7 @@ export const manifests = [
|
|
|
17
17
|
label: 'Rich Text Editor [TinyMCE]',
|
|
18
18
|
propertyEditorSchemaAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS,
|
|
19
19
|
icon: 'icon-tinymce',
|
|
20
|
-
group: '
|
|
20
|
+
group: '#propertyEditorUIGroups_richContent',
|
|
21
21
|
supportsReadOnly: true,
|
|
22
22
|
keywords: ['content', 'article', 'body', 'html', 'wysiwyg', 'richtext', 'rte', 'tinymce', 'editor', 'formatted', 'paragraph'],
|
|
23
23
|
settings: {
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type { Editor } from '@umbraco-cms/backoffice/external/tinymce';
|
|
2
2
|
/**
|
|
3
|
-
* Sizes an image in the editor
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Sizes an image in the editor.
|
|
4
|
+
*
|
|
5
|
+
* By default the image is scaled down to the editor's `maxImageSize`, which is what pasted and
|
|
6
|
+
* dragged images need. Pass `explicitSize` when the size has already been chosen deliberately (for
|
|
7
|
+
* example in the media picker's caption/alt-text dialog) — those dimensions are then used as-is and
|
|
8
|
+
* only the processed image URL is refreshed. Without it, a deliberately chosen size is silently
|
|
9
|
+
* overwritten by the max-size scaling.
|
|
10
|
+
* @param editor The TinyMCE editor instance.
|
|
11
|
+
* @param imageDomElement The image element to size.
|
|
12
|
+
* @param imgUrl Source URL, used to generate a resized `data-mce-src`.
|
|
13
|
+
* @param explicitSize Dimensions to apply verbatim instead of scaling to `maxImageSize`.
|
|
7
14
|
*/
|
|
8
|
-
export declare function sizeImageInEditor(editor: Editor, imageDomElement: HTMLElement, imgUrl?: string
|
|
15
|
+
export declare function sizeImageInEditor(editor: Editor, imageDomElement: HTMLElement, imgUrl?: string, explicitSize?: {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}): Promise<void>;
|
|
9
19
|
/**
|
|
10
20
|
* Scales an image to the max size
|
|
11
21
|
* @param maxSize
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0CAA0C,CAAC;AAEvE
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0CAA0C,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,WAAW,EAC5B,MAAM,CAAC,EAAE,MAAM,EACf,YAAY,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,iBA0ChD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;;EA4B5E;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,iBAqDzE;AAED,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC"}
|
package/dist-cms/utils/index.js
CHANGED
|
@@ -3,14 +3,36 @@
|
|
|
3
3
|
// TODO: This should not be done in this way, we need to split this into separate defined helper methods. This is also very specific to TinyMCE, so should be named that way. [NL]
|
|
4
4
|
import { getProcessedImageUrl } from '@umbraco-cms/backoffice/utils';
|
|
5
5
|
/**
|
|
6
|
-
* Sizes an image in the editor
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Sizes an image in the editor.
|
|
7
|
+
*
|
|
8
|
+
* By default the image is scaled down to the editor's `maxImageSize`, which is what pasted and
|
|
9
|
+
* dragged images need. Pass `explicitSize` when the size has already been chosen deliberately (for
|
|
10
|
+
* example in the media picker's caption/alt-text dialog) — those dimensions are then used as-is and
|
|
11
|
+
* only the processed image URL is refreshed. Without it, a deliberately chosen size is silently
|
|
12
|
+
* overwritten by the max-size scaling.
|
|
13
|
+
* @param editor The TinyMCE editor instance.
|
|
14
|
+
* @param imageDomElement The image element to size.
|
|
15
|
+
* @param imgUrl Source URL, used to generate a resized `data-mce-src`.
|
|
16
|
+
* @param explicitSize Dimensions to apply verbatim instead of scaling to `maxImageSize`.
|
|
10
17
|
*/
|
|
11
|
-
export async function sizeImageInEditor(editor, imageDomElement, imgUrl) {
|
|
18
|
+
export async function sizeImageInEditor(editor, imageDomElement, imgUrl, explicitSize) {
|
|
12
19
|
const size = editor.dom.getSize(imageDomElement);
|
|
13
20
|
const maxImageSize = editor.options.get('maxImageSize');
|
|
21
|
+
if (explicitSize) {
|
|
22
|
+
editor.dom.setAttribs(imageDomElement, {
|
|
23
|
+
width: Math.round(explicitSize.width),
|
|
24
|
+
height: Math.round(explicitSize.height),
|
|
25
|
+
});
|
|
26
|
+
if (imgUrl) {
|
|
27
|
+
const resizedImgUrl = await getProcessedImageUrl(imgUrl, {
|
|
28
|
+
width: Math.round(explicitSize.width),
|
|
29
|
+
height: Math.round(explicitSize.height),
|
|
30
|
+
});
|
|
31
|
+
editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl);
|
|
32
|
+
}
|
|
33
|
+
editor.execCommand('mceAutoResize', false);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
14
36
|
if (maxImageSize && maxImageSize > 0) {
|
|
15
37
|
const newSize = scaleToMaxSize(maxImageSize, size.w, size.h);
|
|
16
38
|
editor.dom.setAttribs(imageDomElement, { width: Math.round(newSize.width), height: Math.round(newSize.height) });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-mce-umbraco/backoffice",
|
|
3
|
-
"version": "17.
|
|
4
|
-
"description": "This Umbraco CMS package enables access to the TinyMCE-based Rich Text Editor (RTE)
|
|
3
|
+
"version": "17.6.2",
|
|
4
|
+
"description": "This Umbraco CMS package enables access to the TinyMCE-based Rich Text Editor (RTE). It requires Umbraco 17.6.2 or later - for Umbraco 16 use the 16.x package versions, for Umbraco 18 use 18.x. It also supports the use of TinyMCE Premium plugins with a valid subscription. Additional features include streamlined configuration for RTE Data Types in Umbraco and enhanced settings that support direct JSON-based configuration via .NET (appsettings.config).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Umbraco",
|
|
7
7
|
"TinyMCE",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@rollup/plugin-json": "^6.1.0",
|
|
41
41
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
42
42
|
"@rollup/plugin-replace": "^6.0.2",
|
|
43
|
-
"@umbraco-cms/backoffice": "^17.
|
|
43
|
+
"@umbraco-cms/backoffice": "^17.6.2",
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"cross-env": "^10.1.0",
|
|
46
46
|
"eslint": "^10.2.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"vite-tsconfig-paths": "^5.1.4"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@umbraco-cms/backoffice": "^17.
|
|
58
|
+
"@umbraco-cms/backoffice": "^17.6.2",
|
|
59
59
|
"tinymce": "^6.8.5",
|
|
60
60
|
"tinymce-i18n": "^24.12.30"
|
|
61
61
|
}
|