diff --git a/src/Umbraco.Web.UI.Client/.gitignore b/src/Umbraco.Web.UI.Client/.gitignore index 105d598d81..8a626a2202 100644 --- a/src/Umbraco.Web.UI.Client/.gitignore +++ b/src/Umbraco.Web.UI.Client/.gitignore @@ -46,4 +46,5 @@ custom-elements.json # JSON for HTML Custom Data # https://github.com/runem/web-component-analyzer#vscode # https://github.com/microsoft/vscode-custom-data -vscode-html-custom-data.json \ No newline at end of file +vscode-html-custom-data.json +public/tinymce/* diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index dd0f9ede2e..537a7e1249 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -18,6 +18,8 @@ "monaco-editor": "^0.36.1", "router-slot": "file:router-slot-2.3.0.tgz", "rxjs": "^7.8.0", + "tinymce": "^6.5.1", + "tinymce-i18n": "^23.5.8", "uuid": "^9.0.0" }, "devDependencies": { @@ -19482,6 +19484,16 @@ "globrex": "^0.1.2" } }, + "node_modules/tinymce": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.5.1.tgz", + "integrity": "sha512-J67fxJiX3tjvVqer1dg1+cOxMeE2P55ESGhaakvqGPbAUU45HnCMLSioaOsxV1KfcXustw9WJo0rtn1SNQlVKQ==" + }, + "node_modules/tinymce-i18n": { + "version": "23.5.8", + "resolved": "https://registry.npmjs.org/tinymce-i18n/-/tinymce-i18n-23.5.8.tgz", + "integrity": "sha512-9ldMYgdiyGb+RdnZqbpOpAhfFXytpss/jXFONQF94oC5zu2xTyv78xuC6iL5dpGPufHMN6h0KLlNbV3ShkFUyw==" + }, "node_modules/title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index be0b55e290..c49c7e9b8d 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -30,6 +30,7 @@ "./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js", "./extension-registry": "./dist-cms/packages/core/extension-registry/index.js", "./id": "./dist-cms/packages/core/id/index.js", + "./macro": "./dist-cms/packages/core/macro/index.js", "./menu": "./dist-cms/packages/core/menu/index.js", "./modal": "./dist-cms/packages/core/modal/index.js", "./notification": "./dist-cms/packages/core/notification/index.js", @@ -129,6 +130,8 @@ "monaco-editor": "^0.36.1", "router-slot": "file:router-slot-2.3.0.tgz", "rxjs": "^7.8.0", + "tinymce": "^6.5.1", + "tinymce-i18n": "^23.5.8", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/src/Umbraco.Web.UI.Client/public-assets/css/rte-content.css b/src/Umbraco.Web.UI.Client/public-assets/css/rte-content.css new file mode 100644 index 0000000000..34ea2b3c77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/public-assets/css/rte-content.css @@ -0,0 +1,42 @@ +.umb-macro-holder { + border: 3px dotted var(--uui-palette-spanish-pink-light); + padding: 7px; + display: block; + margin: 3px; +} + +.umb-macro-holder.loading { + background: url(assets/img/loader.gif) right no-repeat; + background-size: 18px; + background-position-x: 99%; +} + +.umb-embed-holder { + position: relative; +} + +.umb-embed-holder>* { + user-select: none; + pointer-events: none; +} + +.umb-embed-holder[data-mce-selected] { + outline: 2px solid var(--uui-palette-spanish-pink-light); +} + +.umb-embed-holder::before { + z-index: 1000; + width: 100%; + height: 100%; + position: absolute; + content: ' '; +} + +.umb-embed-holder[data-mce-selected]::before { + background: rgba(0, 0, 0, 0.025); +} + +*[data-mce-selected='inline-boundary'] { + background: rgba(0, 0, 0, 0.025); + outline: 2px solid var(--uui-palette-spanish-pink-light); +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts b/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts index 66eddd1fb0..abee27205d 100644 --- a/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/rxjs/index.ts @@ -13,4 +13,5 @@ export { tap, of, lastValueFrom, + firstValueFrom, } from 'rxjs'; diff --git a/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts b/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts new file mode 100644 index 0000000000..d9c64f3c1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tinymce/index.ts @@ -0,0 +1,31 @@ +/** + * TinyMce is a CommonJS module, but in order to make @web/test-runner happy + * we need to load it as a module and then manually register it in the browser + * as a global variable, so that we can find it and use it in our tests. + * We are also loading the default icons, so that we can use them outside of a TinyMce instance. + * + * TODO: Load the plugins that we want to use in the editor. + */ +import * as tiny from 'tinymce'; + +// Declare a global variable to hold the TinyMCE instance +declare global { + interface Window { + tinymce: typeof tiny.default; + } +} + +// Load default icons making them available to everyone +import 'tinymce/icons/default/icons.js'; + +const defaultConfig: tiny.RawEditorOptions = { + base_url: '/umbraco/backoffice/tinymce', +}; + +/* Initialize TinyMCE */ +export function renderEditor(userConfig?: tiny.RawEditorOptions) { + const config = { ...defaultConfig, ...userConfig }; + return window.tinymce.init(config); +} + +export { tiny as tinymce }; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types.ts index d28565a6ee..4fbcb69d09 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types.ts @@ -94,7 +94,6 @@ export interface ManifestClass * @TJS-ignore */ class?: ClassConstructor; - //loader?: () => Promise; } export interface ManifestClassWithClassConstructor extends ManifestClass { @@ -121,8 +120,6 @@ export interface ManifestElement */ elementName?: string; - //loader?: () => Promise; - /** * This contains properties specific to the type of extension */ diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type.data.ts index 970d4b0b42..3d3ddf8594 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type.data.ts @@ -558,7 +558,76 @@ export const data: Array = parentId: null, propertyEditorAlias: 'Umbraco.TinyMCE', propertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE', - values: [], + values: [ + { + alias: 'hideLabel', + value: true, + }, + { alias: 'dimensions', value: { height: 500 } }, + { alias: 'maxImageSize', value: 500 }, + { alias: 'ignoreUserStartNodes', value: false }, + { + alias: 'validElements', + value: + '+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],-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,video[*],audio[*],picture[*],source[*],canvas[*]', + }, + { alias: 'invalidElements', value: 'font' }, + // { alias: 'stylesheets', value: ['/css/rte-content.css'] }, + { + alias: 'toolbar', + value: [ + 'sourcecode', + 'undo', + 'redo', + 'styles', + 'bold', + 'italic', + 'alignleft', + 'aligncenter', + 'alignright', + 'bullist', + 'numlist', + 'outdent', + 'indent', + 'link', + 'unlink', + 'anchor', + 'table', + 'umbmediapicker', + 'umbmacro', + 'umbembeddialog', + ], + }, + { + alias: 'plugins', + value: [ + { + name: 'anchor', + }, + { + name: 'charmap', + }, + { + name: 'table', + }, + { + name: 'lists', + }, + { + name: 'advlist', + }, + { + name: 'autolink', + }, + { + name: 'directionality', + }, + { + name: 'searchreplace', + }, + ], + }, + ], }, { $type: '', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type.data.ts index cb3615e7c9..0fa0dada46 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type.data.ts @@ -21,6 +21,24 @@ export const data: Array = [ isElement: false, properties: [ { + id: '1', + containerId: 'all-properties-group-key', + alias: 'richTextEditor', + name: 'Rich Text editor', + description: '', + dataTypeId: 'dt-richTextEditor', + variesByCulture: false, + variesBySegment: false, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, { id: '2', containerId: 'all-properties-group-id', alias: 'colorPicker', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document.data.ts index afd6279e8f..f67d0b1359 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document.data.ts @@ -19,6 +19,13 @@ export const data: Array = [ id: 'all-property-editors-document-id', contentTypeId: 'all-property-editors-document-type-id', values: [ + { + $type: '', + alias: 'richTextEditor', + culture: null, + segment: null, + value: 'Some value for the RTE with an external link and an internal link foo foo
Macro alias: TestMacro
', + }, { $type: '', alias: 'email', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/template.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/template.data.ts index 74eba67876..27a2e656d5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/template.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/template.data.ts @@ -19,12 +19,14 @@ const createTemplate = (dbItem: TemplateDBItem): TemplateResponseModel => { name: dbItem.name, alias: dbItem.alias, content: dbItem.content, + masterTemplateId: dbItem.masterTemplateId, }; }; const createTemplateItem = (dbItem: TemplateDBItem): TemplateItemResponseModel => ({ name: dbItem.name, id: dbItem.id, + alias: dbItem.alias, }); export const data: Array = [ @@ -70,13 +72,14 @@ export const data: Array = [ id: '9a84c0b3-03b4-4dd4-84ac-706740ac0f72', isContainer: false, parentId: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', + masterTemplateId: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', name: 'Child', type: 'template', icon: 'umb:layout', hasChildren: false, alias: 'Test', content: - '@using Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\r\n@{\r\n\tLayout = null;\r\n}', + '@using Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\r\n@{\r\n\tLayout = "Test.cshtml";\r\n}', }, { $type: '', @@ -84,17 +87,22 @@ export const data: Array = [ isContainer: false, parentId: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', name: 'Has Master Template', + masterTemplateId: '9a84c0b3-03b4-4dd4-84ac-706740ac0f71', type: 'template', icon: 'umb:layout', hasChildren: false, alias: 'hasMasterTemplate', content: - '@using Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\r\n@{\r\n\tLayout = "some/path/to/a/template.cshtml";\r\n}', + '@using Umbraco.Cms.Web.Common.PublishedModels;\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;\r\n@{\r\n\tLayout = "Test.cshtml";\r\n}', }, ]; export const createTemplateScaffold = (masterTemplateAlias: string) => { - return `Template Scaffold Mock for master template: ${masterTemplateAlias}`; + return `@using Umbraco.Cms.Web.Common.PublishedModels; +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + Layout = ${masterTemplateAlias}; +}`; }; export const templateQuerySettings = { @@ -169,10 +177,11 @@ class UmbTemplateData extends UmbEntityData { return item ? createTemplateItem(item) : null; } - getScaffold(): TemplateScaffoldResponseModel { + getScaffold(masterTemplateId: string | null = null): TemplateScaffoldResponseModel { + const masterTemplate = this.data.find((item) => item.id === masterTemplateId); + return { - content: - '@using Umbraco.Cms.Web.Common.PublishedModels;\r\n@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage\r\n@{\r\n\tLayout = null;\r\n}', + content: createTemplateScaffold(masterTemplate?.alias ?? 'null'), }; } diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/images.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/images.handlers.ts new file mode 100644 index 0000000000..c0ef258979 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/images.handlers.ts @@ -0,0 +1,8 @@ +import { rest } from 'msw'; + +// TODO: add schema +export const handlers = [ + rest.get('/umbraco/management/api/v1/images/getprocessedimageurl', (req, res, ctx) => { + return res(ctx.status(200), ctx.json('/url/to/processed/image')); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts index 3a8b5d3891..4edd2140c4 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts @@ -34,7 +34,7 @@ export const manifestDevelopmentHandler = rest.get(umbracoPath('/package/manifes label: 'My Custom Property', icon: 'document', group: 'Common', - propertyEditorModel: 'Umbraco.JSON', + propertyEditorSchema: 'Umbraco.JSON', }, }, ], diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts index 0e79447d08..122e4677e5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/rte-embed.handlers.ts @@ -4,12 +4,18 @@ import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ rest.get(umbracoPath('/rteembed'), (req, res, ctx) => { - const width = req.url.searchParams.get('width') ?? 360; - const height = req.url.searchParams.get('height') ?? 240; + const widthParam = req.url.searchParams.get('width'); + const width = widthParam ? parseInt(widthParam) : 360; + + const heightParam = req.url.searchParams.get('height'); + const height = heightParam ? parseInt(heightParam) : 240; + const response: OEmbedResult = { supportsDimensions: true, markup: ``, oEmbedStatus: OEmbedStatus.Success, + width, + height, }; return res(ctx.status(200), ctx.json(response)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/data-type/data-type-property-collection.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/data-type/data-type-property-collection.class.ts index ef0ce3922c..90eed2632f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/data-type/data-type-property-collection.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/data-type/data-type-property-collection.class.ts @@ -8,7 +8,6 @@ export class UmbDataTypePropertyCollection extends Array = []) { super(...args); } - static get [Symbol.species](): ArrayConstructor { return Array; } @@ -26,4 +25,22 @@ export class UmbDataTypePropertyCollection extends Array x.alias === alias); } + + /** + * Convert the underlying array to an object where + * the property value is keyed by its alias + * eg + * `[ + * { 'alias': 'myProperty', 'value': 27 }, + * { 'alias': 'anotherProperty', 'value': 'eleven' }, + * ]` + * is returned as + * `{ + * myProperty: 27, + * anotherProperty: 'eleven', + * }` + */ + toObject(): Record { + return Object.fromEntries(this.map((x) => [x.alias, x.value])); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index c9ba96ad78..3714842565 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -19,6 +19,7 @@ export * from './input-color/index.js'; export * from './input-eye-dropper/index.js'; export * from './input-list-base/index.js'; export * from './input-multi-url/index.js'; +export * from './input-tiny-mce/index.js'; export * from './input-number-range/index.js'; export * from './input-radio-button-list/index.js'; export * from './input-section/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/index.ts new file mode 100644 index 0000000000..cc2f0fa312 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/index.ts @@ -0,0 +1,6 @@ +export * from './input-tiny-mce.defaults.js'; +export * from './input-tiny-mce.element.js'; +export * from './input-tiny-mce.languages.js'; +export * from './input-tiny-mce.sanitizer.js'; +export * from './input-tiny-mce.handlers.js'; +export * from './tiny-mce-plugin.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.defaults.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.defaults.ts new file mode 100644 index 0000000000..8ff3d02e49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.defaults.ts @@ -0,0 +1,52 @@ +import type { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; + +export type TinyStyleSheet = tinymce.RawEditorOptions['style_formats']; + +export const defaultStyleFormats: TinyStyleSheet = [ + { + title: 'Headers', + items: [ + { title: 'Page header', block: 'h2' }, + { title: 'Section header', block: 'h3' }, + { title: 'Paragraph header', block: 'h4' }, + ], + }, + { + title: 'Blocks', + items: [{ title: 'Normal', block: 'p' }], + }, + { + title: 'Containers', + items: [ + { title: 'Quote', block: 'blockquote' }, + { title: 'Code', block: 'code' }, + ], + }, +]; + +//These are absolutely required in order for the macros to render inline +//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce +export const defaultExtendedValidElements = + '@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|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],span[id|class|style|lang],figure,figcaption'; + +export const defaultFallbackConfig: tinymce.RawEditorOptions = { + toolbar: [ + 'styles', + 'bold', + 'italic', + 'alignleft', + 'aligncenter', + 'alignright', + 'bullist', + 'numlist', + 'outdent', + 'indent', + 'link', + 'umbmediapicker', + 'umbmacro', + 'umbembeddialog', + ], + mode: 'classic', + stylesheets: [], + maxImageSize: 500, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts new file mode 100644 index 0000000000..bebd992be3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.element.ts @@ -0,0 +1,321 @@ +import { defaultExtendedValidElements, defaultFallbackConfig, defaultStyleFormats } from './input-tiny-mce.defaults.js'; +import { pastePreProcessHandler, uploadImageHandler } from './input-tiny-mce.handlers.js'; +import { availableLanguages } from './input-tiny-mce.languages.js'; +import { uriAttributeSanitizer } from './input-tiny-mce.sanitizer.js'; +import { FormControlMixin, UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { renderEditor, type tinymce } from '@umbraco-cms/backoffice/external/tinymce'; +import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; +import { + TinyMcePluginArguments, + UmbDataTypePropertyCollection, + UmbTinyMcePluginBase, +} from '@umbraco-cms/backoffice/components'; +import { ClassConstructor, hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api'; +import { ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { + PropertyValueMap, + css, + customElement, + html, + property, + query, + state, +} from '@umbraco-cms/backoffice/external/lit'; +import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { UMB_MODAL_CONTEXT_TOKEN, UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import { UmbMediaHelper } from '@umbraco-cms/backoffice/utils'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +// TODO => integrate macro picker, update stylesheet fetch when backend CLI exists (ref tinymce.service.js in existing backoffice) +@customElement('umb-input-tiny-mce') +export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { + @property({ type: Object }) + configuration?: UmbDataTypePropertyCollection; + + @state() + private _tinyConfig: tinymce.RawEditorOptions = {}; + + modalContext!: UmbModalContext; + #mediaHelper = new UmbMediaHelper(); + #currentUser?: UmbLoggedInUser; + #auth?: typeof UMB_AUTH.TYPE; + #plugins: Array UmbTinyMcePluginBase> = []; + #editorRef?: tinymce.Editor | null = null; + + protected getFormElement() { + return undefined; + } + + @query('#editor', true) + private _editorElement?: HTMLElement; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (modalContext) => { + this.modalContext = modalContext; + }); + + // TODO => this breaks tests, removing for now will ignore user language + // and fall back to tinymce default language + // this.consumeContext(UMB_AUTH, (instance) => { + // this.#auth = instance; + // this.#observeCurrentUser(); + // }); + } + + async #observeCurrentUser() { + if (!this.#auth) return; + + this.observe(this.#auth.currentUser, (currentUser) => (this.#currentUser = currentUser)); + } + + protected async firstUpdated(_changedProperties: PropertyValueMap | Map): Promise { + super.firstUpdated(_changedProperties); + await this.#loadPlugins(); + await this.#setTinyConfig(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + + if (this.#editorRef) { + this.#editorRef.destroy(); + } + } + + /** + * Load all custom plugins - need to split loading and instantiating as these + * need the editor instance as a ctor argument. If we load them in the editor + * setup method, the asynchronous nature means the editor is loaded before + * the plugins are ready and so are not associated with the editor. + */ + async #loadPlugins() { + const observable = umbExtensionsRegistry?.extensionsOfType('tinyMcePlugin'); + const plugins = (await firstValueFrom(observable)) as ManifestTinyMcePlugin[]; + + for (const plugin of plugins) { + const module = await loadExtension(plugin); + if (hasDefaultExport>(module)) { + this.#plugins.push(module.default); + } + } + } + + async #setTinyConfig() { + // create an object by merging the configuration onto the fallback config + const configurationOptions: Record = { + ...defaultFallbackConfig, + ...(this.configuration ? this.configuration?.toObject() : {}), + }; + + // no auto resize when a fixed height is set + if (!configurationOptions.dimensions?.height) { + configurationOptions.plugins ??= []; + configurationOptions.plugins.splice(configurationOptions.plugins.indexOf('autoresize'), 1); + } + + // set the default values that will not be modified via configuration + this._tinyConfig = { + autoresize_bottom_margin: 10, + body_class: 'umb-rte', + //see https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#cache_suffix + cache_suffix: '?umb__rnd=' + window.Umbraco?.Sys.ServerVariables.application.cacheBuster, // TODO: Cache buster + contextMenu: false, + inline_boundaries_selector: 'a[href],code,.mce-annotation,.umb-embed-holder,.umb-macro-holder', + menubar: false, + paste_remove_styles_if_webkit: true, + paste_preprocess: pastePreProcessHandler, + relative_urls: false, + resize: false, + statusbar: false, + setup: (editor) => this.#editorSetup(editor), + target: this._editorElement, + }; + + // extend with configuration values + this._tinyConfig = { + ...this._tinyConfig, + content_css: configurationOptions.stylesheets.join(','), + extended_valid_elements: defaultExtendedValidElements, + height: configurationOptions.height ?? 500, + invalid_elements: configurationOptions.invalidElements, + plugins: configurationOptions.plugins.map((x: any) => x.name), + toolbar: configurationOptions.toolbar.join(' '), + style_formats: defaultStyleFormats, + valid_elements: configurationOptions.validElements, + width: configurationOptions.width, + }; + + // Need to check if we are allowed to UPLOAD images + // This is done by checking if the insert image toolbar button is available + if (this.#isMediaPickerEnabled()) { + this._tinyConfig = { + ...this._tinyConfig, + // Update the TinyMCE Config object to allow pasting + images_upload_handler: uploadImageHandler, + automatic_uploads: false, + images_replace_blob_uris: false, + // This allows images to be pasted in & stored as Base64 until they get uploaded to server + paste_data_images: true, + }; + } + + this.#setLanguage(); + + if (this.#editorRef) { + this.#editorRef.destroy(); + } + + const editors = await renderEditor(this._tinyConfig); + this.#editorRef = editors.pop(); + } + + /** + * Sets the language to use for TinyMCE */ + #setLanguage() { + const localeId = this.#currentUser?.languageIsoCode; + //try matching the language using full locale format + let languageMatch = availableLanguages.find((x) => localeId?.localeCompare(x) === 0); + + //if no matches, try matching using only the language + if (!languageMatch) { + const localeParts = localeId?.split('_'); + if (localeParts) { + languageMatch = availableLanguages.find((x) => x === localeParts[0]); + } + } + + // only set if language exists, will fall back to tiny default + if (languageMatch) { + this._tinyConfig.language = languageMatch; + } + } + + #editorSetup(editor: tinymce.Editor) { + editor.suffix = '.min'; + + // register custom option maxImageSize + editor.options.register('maxImageSize', { processor: 'number', default: defaultFallbackConfig.maxImageSize }); + + // instantiate plugins - these are already loaded in this.#loadPlugins + // to ensure they are available before setting up the editor. + // Plugins require a reference to the current editor as a param, so can not + // be instantiated until we have an editor + for (const plugin of this.#plugins) { + new plugin({ host: this, editor }); + } + + // define keyboard shortcuts + editor.addShortcut('Ctrl+S', '', () => + this.dispatchEvent(new CustomEvent('rte.shortcut.save', { composed: true, bubbles: true })) + ); + + editor.addShortcut('Ctrl+P', '', () => + this.dispatchEvent(new CustomEvent('rte.shortcut.saveAndPublish', { composed: true, bubbles: true })) + ); + + // bind editor events + editor.on('init', () => this.#onInit(editor)); + editor.on('Change', () => this.#onChange(editor.getContent())); + editor.on('Dirty', () => this.#onChange(editor.getContent())); + editor.on('Keyup', () => this.#onChange(editor.getContent())); + editor.on('SetContent', () => this.#mediaHelper.uploadBlobImages(editor)); + + editor.on('focus', () => this.dispatchEvent(new CustomEvent('umb-rte-focus', { composed: true, bubbles: true }))); + + editor.on('blur', () => { + this.#onChange(editor.getContent()); + this.dispatchEvent(new CustomEvent('umb-rte-blur', { composed: true, bubbles: true })); + }); + + editor.on('ObjectResized', (e) => { + this.#mediaHelper.onResize(e); + this.#onChange(editor.getContent()); + }); + + editor.on('init', () => editor.setContent(this.value?.toString() ?? '')); + + // If we can not find the insert image/media toolbar button + // Then we need to add an event listener to the editor + // That will update native browser drag & drop events + // To update the icon to show you can NOT drop something into the editor + if (this._tinyConfig.toolbar && !this.#isMediaPickerEnabled()) { + // Wire up the event listener + editor.on('dragstart dragend dragover draggesture dragdrop drop drag', (e: tinymce.EditorEvent) => { + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'none'; + e.dataTransfer.dropEffect = 'none'; + } + e.stopPropagation(); + }); + } + } + + #onInit(editor: tinymce.Editor) { + //enable browser based spell checking + editor.getBody().setAttribute('spellcheck', 'true'); + uriAttributeSanitizer(editor); + } + + #onChange(value: string) { + super.value = value; + this.dispatchEvent(new CustomEvent('change')); + } + + #isMediaPickerEnabled() { + const toolbar = this._tinyConfig.toolbar; + if (Array.isArray(toolbar) && (toolbar as string[]).includes('umbmediapicker')) { + return true; + } else if (typeof toolbar === 'string' && toolbar.includes('umbmediapicker')) { + return true; + } + + return false; + } + + /** + * Nothing rendered by default - TinyMCE initialisation creates + * a target div and binds the RTE to that element + * @returns + */ + render() { + return html`
`; + } + + static styles = [ + UUITextStyles, + css` + #editor { + position: relative; + min-height: 100px; + } + + .tox-tinymce-aux { + z-index: 9000; + } + + .tox-tinymce-inline { + z-index: 900; + } + + .tox-tinymce-fullscreen { + position: absolute; + } + + /* FIXME: Remove this workaround when https://github.com/tinymce/tinymce/issues/6431 has been fixed */ + .tox .tox-collection__item-label { + line-height: 1 !important; + } + `, + ]; +} + +export default UmbInputTinyMceElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-tiny-mce': UmbInputTinyMceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.handlers.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.handlers.ts new file mode 100644 index 0000000000..7fbe718a3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.handlers.ts @@ -0,0 +1,54 @@ +import type { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; + +export const pastePreProcessHandler: tinymce.RawEditorOptions['paste_preprocess'] = (_editor, args) => { + // Remove spans + args.content = args.content.replace(/<\s*span[^>]*>(.*?)<\s*\/\s*span>/g, '$1'); + // Convert b to strong. + args.content = args.content.replace(/<\s*b([^>]*)>(.*?)<\s*\/\s*b([^>]*)>/g, '$2'); + // convert i to em + args.content = args.content.replace(/<\s*i([^>]*)>(.*?)<\s*\/\s*i([^>]*)>/g, '$2'); +}; + +export const uploadImageHandler: tinymce.RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', window.Umbraco?.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage'); + + xhr.onloadstart = () => + document.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true })); + + xhr.onloadend = () => + document.dispatchEvent(new CustomEvent('rte.file.uploaded', { composed: true, bubbles: true })); + + xhr.upload.onprogress = (e) => progress((e.loaded / e.total) * 100); + + xhr.onerror = () => reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status); + + xhr.onload = () => { + if (xhr.status < 200 || xhr.status >= 300) { + reject('HTTP Error: ' + xhr.status); + return; + } + + const json = JSON.parse(xhr.responseText); + + if (!json || typeof json.tmpLocation !== 'string') { + reject('Invalid JSON: ' + xhr.responseText); + return; + } + + // Put temp location into localstorage (used to update the img with data-tmpimg later on) + localStorage.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation); + + // We set the img src url to be the same as we started + // The Blob URI is stored in TinyMce's cache + // so the img still shows in the editor + resolve(blobInfo.blobUri()); + }; + + const formData = new FormData(); + formData.append('file', blobInfo.blob(), blobInfo.blob().name); + + xhr.send(formData); + }); +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.languages.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.languages.ts new file mode 100644 index 0000000000..d2a8129f44 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.languages.ts @@ -0,0 +1,78 @@ +export const availableLanguages = [ + 'ar', + 'ar_SA', + 'hy', + 'az', + 'eu', + 'be', + 'bn_BD', + 'bs', + 'bg_BG', + 'ca', + 'zh_CN', + 'zh_TW', + 'hr', + 'cs', + 'da', + 'dv', + 'nl', + 'en_CA', + 'en_GB', + 'et', + 'fo', + 'fi', + 'fr_FR', + 'gd', + 'gl', + 'ka_GE', + 'de', + 'de_AT', + 'el', + 'he_IL', + 'hi_IN', + 'hu_HU', + 'is_IS', + 'id', + 'it', + 'ja', + 'kab', + 'kk', + 'km_KH', + 'ko_KR', + 'ku', + 'ku_IQ', + 'lv', + 'lt', + 'lb', + 'ml', + 'ml_IN', + 'mn_MN', + 'nb_NO', + 'fa', + 'fa_IR', + 'pl', + 'pt_BR', + 'pt_PT', + 'ro', + 'ru', + 'sr', + 'si_LK', + 'sk', + 'sl_SI', + 'es', + 'es_MX', + 'sv_SE', + 'tg', + 'ta', + 'ta_IN', + 'tt', + 'th_TH', + 'tr', + 'tr_TR', + 'ug', + 'uk', + 'uk_UA', + 'vi', + 'vi_VN', + 'cy', +]; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.sanitizer.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.sanitizer.ts new file mode 100644 index 0000000000..1361890838 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/input-tiny-mce.sanitizer.ts @@ -0,0 +1,60 @@ +import { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; + +/** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes: + * https://github.com/advisories/GHSA-w7jx-j77m-wp65 + * https://github.com/advisories/GHSA-5vm8-hhgr-jcjp + */ +export const uriAttributeSanitizer = (editor: tinymce.Editor) => { + const uriAttributesToSanitize = ['src', 'href', 'data', 'background', 'action', 'formaction', 'poster', 'xlink:href']; + + const parseUri = (function () { + // Encapsulated JS logic. + const safeSvgDataUrlElements = ['img', 'video']; + const scriptUriRegExp = /((java|vb)script|mhtml):/i; + // eslint-disable-next-line no-control-regex + const trimRegExp = /[\s\u0000-\u001F]+/g; + + const isInvalidUri = (uri: string, tagName: string) => { + if (/^data:image\//i.test(uri)) { + return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri); + } else { + return /^data:/i.test(uri); + } + }; + + return function parseUri(uri: string, tagName: string) { + uri = uri.replace(trimRegExp, ''); + try { + // Might throw malformed URI sequence + uri = decodeURIComponent(uri); + } catch (ex) { + // Fallback to non UTF-8 decoder + uri = unescape(uri); + } + + if (scriptUriRegExp.test(uri)) { + return; + } + + if (isInvalidUri(uri, tagName)) { + return; + } + + return uri; + }; + })(); + + if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) { + uriAttributesToSanitize.forEach((attribute) => { + editor.serializer.addAttributeFilter(attribute, (nodes: tinymce.AstNode[]) => { + nodes.forEach((node: tinymce.AstNode) => { + node.attributes?.forEach((attr) => { + if (uriAttributesToSanitize.includes(attr.name.toLowerCase())) { + attr.value = parseUri(attr.value, node.name) ?? ''; + } + }); + }); + }); + }); + } +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/tiny-mce-plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/tiny-mce-plugin.ts new file mode 100644 index 0000000000..3f8f7715be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-tiny-mce/tiny-mce-plugin.ts @@ -0,0 +1,19 @@ +import type { UmbDataTypePropertyCollection, UmbInputTinyMceElement } from '@umbraco-cms/backoffice/components'; +import type { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; + +export class UmbTinyMcePluginBase { + host: UmbInputTinyMceElement; + editor: tinymce.Editor; + configuration?: UmbDataTypePropertyCollection; + + constructor(arg: TinyMcePluginArguments) { + this.host = arg.host; + this.editor = arg.editor; + this.configuration = arg.host.configuration; + } +} + +export interface TinyMcePluginArguments { + host: UmbInputTinyMceElement; + editor: tinymce.Editor; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/property-editor-config/property-editor-config.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/property-editor-config/property-editor-config.element.ts index 96d90a7779..196838b67b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/property-editor-config/property-editor-config.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/property-editor-config/property-editor-config.element.ts @@ -45,12 +45,12 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement { @state() private _properties: Array = []; - private _propertyEditorModelConfigDefaultData: Array = []; + private _propertyEditorSchemaConfigDefaultData: Array = []; private _propertyEditorUISettingsDefaultData: Array = []; private _configDefaultData?: Array; - private _propertyEditorModelConfigProperties: Array = []; + private _propertyEditorSchemaConfigProperties: Array = []; private _propertyEditorUISettingsProperties: Array = []; private _observePropertyEditorUIConfig() { @@ -59,7 +59,7 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement { this.observe( umbExtensionsRegistry.getByTypeAndAlias('propertyEditorUi', this.propertyEditorUiAlias), (manifest) => { - this._observePropertyEditorModelConfig(manifest?.meta.propertyEditorAlias); + this._observePropertyEditorSchemaConfig(manifest?.meta.propertyEditorSchemaAlias); this._propertyEditorUISettingsProperties = manifest?.meta.settings?.properties || []; this._propertyEditorUISettingsDefaultData = manifest?.meta.settings?.defaultData || []; this._mergeConfigProperties(); @@ -68,24 +68,27 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement { ); } - private _observePropertyEditorModelConfig(propertyEditorAlias?: string) { - if (!propertyEditorAlias) return; + private _observePropertyEditorSchemaConfig(propertyEditorSchemaAlias?: string) { + if (!propertyEditorSchemaAlias) return; - this.observe(umbExtensionsRegistry.getByTypeAndAlias('propertyEditorModel', propertyEditorAlias), (manifest) => { - this._propertyEditorModelConfigProperties = manifest?.meta.settings?.properties || []; - this._propertyEditorModelConfigDefaultData = manifest?.meta.settings?.defaultData || []; - this._mergeConfigProperties(); - this._mergeConfigDefaultData(); - }); + this.observe( + umbExtensionsRegistry.getByTypeAndAlias('propertyEditorSchema', propertyEditorSchemaAlias), + (manifest) => { + this._propertyEditorSchemaConfigProperties = manifest?.meta.settings?.properties || []; + this._propertyEditorSchemaConfigDefaultData = manifest?.meta.settings?.defaultData || []; + this._mergeConfigProperties(); + this._mergeConfigDefaultData(); + } + ); } private _mergeConfigProperties() { - this._properties = [...this._propertyEditorModelConfigProperties, ...this._propertyEditorUISettingsProperties]; + this._properties = [...this._propertyEditorSchemaConfigProperties, ...this._propertyEditorUISettingsProperties]; } private _mergeConfigDefaultData() { this._configDefaultData = [ - ...this._propertyEditorModelConfigDefaultData, + ...this._propertyEditorSchemaConfigDefaultData, ...this._propertyEditorUISettingsDefaultData, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts index 995652cfb7..ff93a45bb2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/property-type-based-property/property-type-based-property.element.ts @@ -98,13 +98,13 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { if (!this._propertyEditorUiAlias && dataType?.propertyEditorAlias) { //use 'dataType.propertyEditorAlias' to look up the extension in the registry: this.observe( - umbExtensionsRegistry.getByTypeAndAlias('propertyEditorModel', dataType.propertyEditorAlias), + umbExtensionsRegistry.getByTypeAndAlias('propertyEditorSchema', dataType.propertyEditorAlias), (extension) => { if (!extension) return; this._propertyEditorUiAlias = extension?.meta.defaultPropertyEditorUiAlias; - this.removeControllerByUnique('_observePropertyEditorModel'); + this.removeControllerByUnique('_observePropertyEditorSchema'); }, - '_observePropertyEditorModel' + '_observePropertyEditorSchema' ); } }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.element.ts index d4c2e261a1..9cc6595a5e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.element.ts @@ -26,8 +26,8 @@ export class UmbRefPropertyEditorUIElement extends UUIRefNodeElement { * @attr * @default '' */ - @property({ type: String, attribute: 'property-editor-model-alias' }) - propertyEditorModelAlias = ''; + @property({ type: String, attribute: 'property-editor-schema-alias' }) + propertyEditorSchemaAlias = ''; protected renderDetail() { const details: string[] = []; @@ -36,8 +36,8 @@ export class UmbRefPropertyEditorUIElement extends UUIRefNodeElement { details.push(this.alias); } - if (this.propertyEditorModelAlias !== '') { - details.push(this.propertyEditorModelAlias); + if (this.propertyEditorSchemaAlias !== '') { + details.push(this.propertyEditorSchemaAlias); } else { details.push('Property Editor Missing'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts index f64615e2ec..1bbbfd4d73 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-property-editor-ui/ref-property-editor-ui.stories.ts @@ -15,7 +15,7 @@ export const Overview: Story = { args: { name: 'Custom Property Editor UI', alias: 'Umb.PropertyEditorUi.CustomUI', - propertyEditorModelAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', }, }; @@ -23,7 +23,7 @@ export const WithDetail: Story = { args: { name: 'Custom Property Editor UI', alias: 'Umb.PropertyEditorUi.CustomUI', - propertyEditorModelAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', detail: 'With some custom details', }, }; @@ -32,14 +32,14 @@ export const WithSlots: Story = { args: { name: 'Custom Property Editor UI', alias: 'Umb.PropertyEditorUi.CustomUI', - propertyEditorModelAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', detail: 'With some custom details', }, render: (args) => html`
10
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts index 7cdb6d1d10..6d838deeba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts @@ -11,13 +11,14 @@ import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.mod import type { ManifestModal } from './modal.model.js'; import type { ManifestPackageView } from './package-view.model.js'; import type { ManifestPropertyAction } from './property-action.model.js'; -import type { ManifestPropertyEditorUi, ManifestPropertyEditorModel } from './property-editor.model.js'; +import type { ManifestPropertyEditorUi, ManifestPropertyEditorSchema } from './property-editor.model.js'; import type { ManifestRepository } from './repository.model.js'; import type { ManifestSection } from './section.model.js'; import type { ManifestSectionSidebarApp, ManifestSectionSidebarAppMenuKind } from './section-sidebar-app.model.js'; import type { ManifestSectionView } from './section-view.model.js'; import type { ManifestStore, ManifestTreeStore, ManifestItemStore } from './store.model.js'; import type { ManifestTheme } from './theme.model.js'; +import type { ManifestTinyMcePlugin } from './tinymce-plugin.model.js'; import type { ManifestTree } from './tree.model.js'; import type { ManifestTreeItem } from './tree-item.model.js'; import type { ManifestUserProfileApp } from './user-profile-app.model.js'; @@ -47,6 +48,7 @@ export * from './section-view.model.js'; export * from './section.model.js'; export * from './store.model.js'; export * from './theme.model.js'; +export * from './tinymce-plugin.model.js'; export * from './tree-item.model.js'; export * from './tree.model.js'; export * from './user-profile-app.model.js'; @@ -74,7 +76,7 @@ export type ManifestTypes = | ManifestModal | ManifestPackageView | ManifestPropertyAction - | ManifestPropertyEditorModel + | ManifestPropertyEditorSchema | ManifestPropertyEditorUi | ManifestRepository | ManifestSection @@ -83,6 +85,7 @@ export type ManifestTypes = | ManifestSectionView | ManifestStore | ManifestTheme + | ManifestTinyMcePlugin | ManifestTree | ManifestTreeItem | ManifestTreeStore diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-editor.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-editor.model.ts index 3ff0fe102e..5b76646ad0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-editor.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-editor.model.ts @@ -9,7 +9,7 @@ export interface ManifestPropertyEditorUi extends ManifestElement; +} + +/** + * The manifest for a TinyMCE plugin. + * The plugin will be loaded into the TinyMCE editor. + * A plugin can add things like buttons, menu items, context menu items, etc. through the TinyMCE API. + * A plugin can also add custom commands to the editor. + * A plugin can also modify the behavior of the editor. + * + * @see [TinyMCE Plugin](https://www.tiny.cloud/docs/tinymce/6/apis/tinymce.plugin/) for more information. + */ +export interface ManifestTinyMcePlugin extends ManifestClass { + type: 'tinyMcePlugin'; + meta?: MetaTinyMcePlugin; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/index.ts index 1f3d426742..8e276d67ee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/index.ts @@ -2,6 +2,7 @@ import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerE import { manifests as debugManifests } from './debug/manifests.js'; import { manifests as propertyActionManifests } from './property-actions/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; +import { manifests as tinyMcePluginManifests } from './property-editors/uis/tiny-mce/plugins/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as modalManifests } from './modal/common/manifests.js'; import { UmbStoreExtensionInitializer } from './store/index.js'; @@ -21,6 +22,7 @@ export * from './entity-action/index.js'; export * from './entity-bulk-action/index.js'; export * from './extension-registry/index.js'; export * from './id/index.js'; +export * from './macro/index.js'; export * from './menu/index.js'; export * from './modal/index.js'; export * from './notification/index.js'; @@ -38,6 +40,7 @@ const manifests: Array = [ ...debugManifests, ...propertyActionManifests, ...propertyEditorManifests, + ...tinyMcePluginManifests, ...workspaceManifests, ...modalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/macro/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/macro/index.ts new file mode 100644 index 0000000000..f8f2528b5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/macro/index.ts @@ -0,0 +1 @@ +export * from './macro.service.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/macro/macro.service.ts b/src/Umbraco.Web.UI.Client/src/packages/core/macro/macro.service.ts new file mode 100644 index 0000000000..11c02f9f90 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/macro/macro.service.ts @@ -0,0 +1,148 @@ +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export interface MacroSyntaxData { + macroAlias: string; + macroParamsDictionary: { [key: string]: string }; + syntax?: string; +} + +export class UmbMacroService { + /** parses the special macro syntax like + * and returns an object with the macro alias and it's parameters + * */ + parseMacroSyntax(syntax = '') { + //This regex will match an alias of anything except characters that are quotes or new lines (for legacy reasons, when new macros are created + // their aliases are cleaned an invalid chars are stripped) + const expression = + /(<\?UMBRACO_MACRO (?:.+?)?macroAlias=["']([^"'\n\r]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; + const match = expression.exec(syntax); + + if (!match || match.length < 3) { + return null; + } + + const macroAlias = match[2]; + + //this will leave us with just the parameters + const paramsChunk = match[1] + .trim() + .replace(new RegExp(`UMBRACO_MACRO macroAlias=["']${macroAlias}["']`), '') + .trim(); + const paramExpression = /(\w+?)=['"]([\s\S]*?)['"]/g; + + const returnVal: MacroSyntaxData = { + macroAlias, + macroParamsDictionary: {}, + }; + + let paramMatch; + while ((paramMatch = paramExpression.exec(paramsChunk))) { + returnVal.macroParamsDictionary[paramMatch[1]] = paramMatch[2]; + } + + return returnVal; + } + + /** + * generates the syntax for inserting a macro into a rich text editor - this is the very old umbraco style syntax * + * @param {MacroSyntaxData} args an object containing the macro alias and it's parameter values + */ + generateMacroSyntax(args: MacroSyntaxData) { + let macroString = `'; + + return macroString; + } + + /** + * generates the syntax for inserting a macro into an mvc template * + * @param {object} args an object containing the macro alias and it's parameter values + */ + generateMvcSyntax(args: MacroSyntaxData) { + let macroString = `@await Umbraco.RenderMacroAsync("${args.macroAlias}"`; + let hasParams = false; + let paramString = ''; + + if (args.macroParamsDictionary) { + paramString = ', new {'; + + for (const [key, val] of Object.entries(args.macroParamsDictionary)) { + hasParams = true; + + const keyVal = `${key}="${val ? val : ''}", `; + + paramString += keyVal; + } + + //remove the last , and trailing whitespace + paramString = paramString.trimEnd().replace(/,*$/, ''); + paramString += '}'; + } + + if (hasParams) { + macroString += paramString; + } + + macroString += ')'; + + return macroString; + } + + collectValueData(macro: any, macroParams: any, renderingEngine: any) { + const macroParamsDictionary: { [key: string]: string } = {}; + const macroAlias = macro.alias; + if (!macroAlias) { + throw 'The macro object does not contain an alias'; + } + + macroParams.forEach((item: any) => { + let val = item.value; + if (item.value !== null && item.value !== undefined && typeof item.value !== 'string') { + try { + val = JSON.parse(val); + } catch (e) { + // not json + } + } + + //each value needs to be xml escaped!! since the value get's stored as an xml attribute + macroParamsDictionary[item.alias] = encodeURIComponent(val); + }); + + let syntax; + + //get the syntax based on the rendering engine + if (renderingEngine && renderingEngine.toLowerCase() === 'mvc') { + syntax = this.generateMvcSyntax({ macroAlias, macroParamsDictionary }); + } else { + syntax = this.generateMacroSyntax({ macroAlias, macroParamsDictionary }); + } + + return { + macroParamsDictionary, + macroAlias, + syntax, + } as MacroSyntaxData; + } +} + +export const UMB_MACRO_SERVICE_CONTEXT_TOKEN = new UmbContextToken(UmbMacroService.name); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/code-editor/code-editor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/code-editor/code-editor-modal.element.ts new file mode 100644 index 0000000000..83498c0147 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/code-editor/code-editor-modal.element.ts @@ -0,0 +1,101 @@ +import { css, html, ifDefined, customElement, query } from '@umbraco-cms/backoffice/external/lit'; +import { loadCodeEditor, type UmbCodeEditorElement } from '@umbraco-cms/backoffice/code-editor'; +import { UUITextStyles, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbCodeEditorModalData, UmbCodeEditorModalResult } from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('umb-code-editor-modal') +export class UmbCodeEditorModalElement extends UmbModalBaseElement { + #isCodeEditorReady = new UmbBooleanState(false); + isCodeEditorReady = this.#isCodeEditorReady.asObservable(); + + @query('umb-code-editor') + _codeEditor?: UmbCodeEditorElement; + + constructor() { + super(); + + this.#loadCodeEditor(); + } + + #handleConfirm() { + this.modalContext?.submit({ content: this.data?.content ?? '' }); + } + + #handleCancel() { + this.modalContext?.reject(); + } + + async #loadCodeEditor() { + try { + await loadCodeEditor(); + this.#isCodeEditorReady.next(true); + } catch (error) { + console.error(error); + } + } + + // TODO => debounce? + #onCodeEditorInput(e: UUIInputEvent) { + e.preventDefault(); + if (!this.data) { + return; + } + + this.data.content = this._codeEditor?.code ?? ''; + } + + #renderCodeEditor() { + return html``; + } + + #renderLoading() { + return html`
+ +
`; + } + + render() { + return html` + +
${this.isCodeEditorReady ? this.#renderCodeEditor() : this.#renderLoading()}
+
+ Cancel + +
+
+ `; + } + + static styles = [ + UUITextStyles, + css` + #editor-box { + padding: var(--uui-box-default-padding, var(--uui-size-space-5, 18px)); + height: 100%; + display: flex; + } + + umb-code-editor { + width: 100%; + } + `, + ]; +} + +export default UmbCodeEditorModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-code-editor-modal': UmbCodeEditorModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/code-editor/code-editor-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/code-editor/code-editor-modal.stories.ts new file mode 100644 index 0000000000..38016588f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/code-editor/code-editor-modal.stories.ts @@ -0,0 +1,24 @@ +import '../confirm/confirm-modal.element.js'; + +import { Meta, Story } from '@storybook/web-components'; +import { html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbCodeEditorModalData } from '@umbraco-cms/backoffice/modal'; + +export default { + title: 'API/Modals/Layouts/Code Editor', + component: 'umb-code-editor-modal', + id: 'umb-code-editor-modal', +} as Meta; + +const data: UmbCodeEditorModalData = { + headline: 'Code editor modal example', + content: `This example opens an HTML string in the Code Editor modal.`, + language: 'html', +}; + +export const Overview: Story = () => html` + + +`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts index 1007034938..ff9deff8f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/embedded-media/embedded-media-modal.element.ts @@ -5,10 +5,9 @@ import { OEmbedStatus, UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalResult, - UmbModalContext, } from '@umbraco-cms/backoffice/modal'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UmbModalBaseElement } from '@umbraco-cms/internal/modal'; interface UmbEmbeddedMediaModalModel { url?: string; @@ -22,18 +21,18 @@ interface UmbEmbeddedMediaModalModel { } @customElement('umb-embedded-media-modal') -export class UmbEmbeddedMediaModalElement extends UmbLitElement { +export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement { #loading = false; #embedResult!: OEmbedResult; - @property({ attribute: false }) - modalContext?: UmbModalContext; - - @property({ type: Object }) - data?: UmbEmbeddedMediaModalData; - #handleConfirm() { - this.modalContext?.submit({ selection: this.#embedResult }); + this.modalContext?.submit({ + preview: this.#embedResult.markup, + originalWidth: this._model.width, + originalHeight: this._model.originalHeight, + width: this.#embedResult.width, + height: this.#embedResult.height, + }); } #handleCancel() { @@ -180,7 +179,7 @@ export class UmbEmbeddedMediaModalElement extends UmbLitElement { () => html`
${when(this.#loading, () => html``)} - ${when(this.#embedResult.markup, () => html`${unsafeHTML(this.#embedResult.markup)}`)} + ${when(this.#embedResult?.markup, () => html`${unsafeHTML(this.#embedResult.markup)}`)} ${when(this._model.info, () => html` `)} ${when(this._model.a11yInfo, () => html` `)}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts index fe7ee898f6..6d7044fd52 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts @@ -43,6 +43,12 @@ const modals: Array = [ name: 'Template Modal', loader: () => import('./template/template-modal.element.js'), }, + { + type: 'modal', + alias: 'Umb.Modal.CodeEditor', + name: 'Code Editor Modal', + loader: () => import('./code-editor/code-editor-modal.element.js'), + }, { type: 'modal', alias: 'Umb.Modal.EmbeddedMedia', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/code-editor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/code-editor-modal.token.ts new file mode 100644 index 0000000000..39fdbd4192 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/code-editor-modal.token.ts @@ -0,0 +1,24 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +// TODO => investigate why exporting CodeEditorLanguage in code-editor barrel +// causes the schema generation task to fail... For now, language property below +// duplicates the CodeEditorLanguage type +export interface UmbCodeEditorModalData { + headline: string; + content: string; + language: 'razor' | 'typescript' | 'javascript' | 'css' | 'markdown' | 'json' | 'html'; + color?: 'positive' | 'danger'; + confirmLabel?: string; +} + +export interface UmbCodeEditorModalResult { + content: string; +} + +export const UMB_CODE_EDITOR_MODAL = new UmbModalToken( + 'Umb.Modal.CodeEditor', + { + type: 'sidebar', + size: 'large', + } +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts index 610b935d80..3444d5aaf8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/embedded-media-modal.token.ts @@ -6,9 +6,9 @@ export enum OEmbedStatus { Success, } -interface UmbEmbeddedMediaDimensions { - width?: number; - height?: number; +export interface UmbEmbeddedMediaDimensions { + width: number; + height: number; constrain?: boolean; } @@ -22,14 +22,16 @@ export interface OEmbedResult extends UmbEmbeddedMediaDimensions { markup?: string; } -export type UmbEmbeddedMediaModalResult = { - selection: OEmbedResult; +export interface UmbEmbeddedMediaModalResult extends UmbEmbeddedMediaModalData { + preview?: string; + originalWidth: number; + originalHeight: number; }; export const UMB_EMBEDDED_MEDIA_MODAL = new UmbModalToken( 'Umb.Modal.EmbeddedMedia', { type: 'sidebar', - size: 'small', + size: 'medium', } ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts index 0c5dfd5cb9..b496bee08c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts @@ -1,6 +1,7 @@ export * from './modal-token.js'; export * from './allowed-document-types-modal.token.js'; export * from './change-password-modal.token.js'; +export * from './code-editor-modal.token.js'; export * from './confirm-modal.token.js'; export * from './create-dictionary-modal.token.js'; export * from './create-user-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/manifests.ts index b4c02695a5..2833682968 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/manifests.ts @@ -1,4 +1,4 @@ -import { manifests as propertyEditorModelManifests } from './models/manifests.js'; +import { manifests as propertyEditorSchemaManifests } from './models/manifests.js'; import { manifests as propertyEditorUIManifests } from './uis/manifests.js'; -export const manifests = [...propertyEditorModelManifests, ...propertyEditorUIManifests]; +export const manifests = [...propertyEditorSchemaManifests, ...propertyEditorUIManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockGrid.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockGrid.ts index 01e8378822..1c77ff966a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockGrid.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockGrid.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Block Grid', alias: 'Umbraco.BlockGrid', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockList.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockList.ts index b610210ecf..006aa588e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockList.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.BlockList.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Block List', alias: 'Umbraco.BlockList', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.CheckboxList.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.CheckboxList.ts index e3675129dc..1999866902 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.CheckboxList.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.CheckboxList.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Checkbox List', alias: 'Umbraco.CheckboxList', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.EyeDropper.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.EyeDropper.ts index abdb5ac0c7..49e5c38dc3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.EyeDropper.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.EyeDropper.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Eye Dropper Color Picker', alias: 'Umbraco.ColorPicker.EyeDropper', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.ts index 3374c860dc..00af93e6f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ColorPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Color Picker', alias: 'Umbraco.ColorPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.DateTime.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.DateTime.ts index 6fdcd50854..4bf4469b90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.DateTime.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.DateTime.ts @@ -1,8 +1,8 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; // TODO: We won't include momentjs anymore so we need to find a way to handle date formats -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Date/Time', alias: 'Umbraco.DateTime', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Decimal.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Decimal.ts index 01db533803..409d27a68e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Decimal.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Decimal.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Decimal', alias: 'Umbraco.Decimal', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Dropdown.Flexible.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Dropdown.Flexible.ts index fbff8e54e8..5729d1ec1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Dropdown.Flexible.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Dropdown.Flexible.ts @@ -1,8 +1,8 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; // TODO: We won't include momentjs anymore so we need to find a way to handle date formats -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Dropdown', alias: 'Umbraco.DropDown.Flexible', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.EmailAddress.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.EmailAddress.ts index da85ecf29b..499d2f12ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.EmailAddress.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.EmailAddress.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Email Address', alias: 'Umbraco.EmailAddress', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.IconPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.IconPicker.ts index 23af75f11f..b9f63ab377 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.IconPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.IconPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Icon Picker', alias: 'Umbraco.IconPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ImageCropper.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ImageCropper.ts index ad0d2788a7..9b17eaf445 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ImageCropper.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ImageCropper.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Email Address', alias: 'Umbraco.ImageCropper', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Integer.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Integer.ts index f6d4ba9f9c..726fd458d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Integer.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Integer.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Decimal', alias: 'Umbraco.Integer', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.JSON.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.JSON.ts index 0816b48da0..204a8df2ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.JSON.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.JSON.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'JSON model', alias: 'Umbraco.JSON', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Label.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Label.ts index ae577da600..06168839d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Label.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Label.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Label', alias: 'Umbraco.Label', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ListView.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ListView.ts index a52179f025..d4d84e4408 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ListView.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.ListView.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'List View', alias: 'Umbraco.ListView', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MarkdownEditor.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MarkdownEditor.ts index 44d0e760e6..7d631b6420 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MarkdownEditor.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MarkdownEditor.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Markdown Editor', alias: 'Umbraco.MarkdownEditor', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MediaPicker3.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MediaPicker3.ts index c95c71bce8..e0e21070d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MediaPicker3.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MediaPicker3.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Media Picker 3', alias: 'Umbraco.MediaPicker3', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberGroupPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberGroupPicker.ts index d75f97872a..883634b5ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberGroupPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberGroupPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Member Group Picker', alias: 'Umbraco.MemberGroupPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberPicker.ts index 2ab6c5f388..f625f129be 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MemberPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Member Picker', alias: 'Umbraco.MemberPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiNodeTreePicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiNodeTreePicker.ts index 2d29b348e9..0f3154f651 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiNodeTreePicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiNodeTreePicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Multi Node Tree Picker', alias: 'Umbraco.MultiNodeTreePicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiUrlPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiUrlPicker.ts index e73d108dc0..e39d682860 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiUrlPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultiUrlPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Multi URL Picker', alias: 'Umbraco.MultiUrlPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultipleTextString.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultipleTextString.ts index fe4c6c8097..9cd0df27c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultipleTextString.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.MultipleTextString.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Multiple Text String', alias: 'Umbraco.MultipleTextString', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RadioButtonList.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RadioButtonList.ts index 0db2abb135..0f7b7db6c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RadioButtonList.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RadioButtonList.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Radio Button List', alias: 'Umbraco.RadioButtonList', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TinyMCE.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RichText.ts similarity index 61% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TinyMCE.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RichText.ts index ac3b78c1b1..f5de80e08f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TinyMCE.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.RichText.ts @@ -1,8 +1,8 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', - name: 'Tiny MCE', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', + name: 'Rich Text', alias: 'Umbraco.TinyMCE', meta: { defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE', @@ -11,6 +11,7 @@ export const manifest: ManifestPropertyEditorModel = { { alias: 'mediaParentId', label: 'Image Upload Folder', + description: 'Choose the upload location of pasted images', propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker', }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Slider.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Slider.ts index 6c5172b077..264643e252 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Slider.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Slider.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Slider', alias: 'Umbraco.Slider', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Tags.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Tags.ts index b84532fb86..e47115ced2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Tags.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.Tags.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Tags', alias: 'Umbraco.Tags', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextArea.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextArea.ts index be1525eb3c..2097d90f7b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextArea.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextArea.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Textarea', alias: 'Umbraco.TextArea', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextBox.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextBox.ts index 2d631d5ec2..7c0470461a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextBox.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TextBox.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Text Box', alias: 'Umbraco.TextBox', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TrueFalse.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TrueFalse.ts index 699ed9d9cf..faa04eaf19 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TrueFalse.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.TrueFalse.ts @@ -1,8 +1,8 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; // TODO: We won't include momentjs anymore so we need to find a way to handle date formats -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Date/Time', alias: 'Umbraco.TrueFalse', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UploadField.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UploadField.ts index 4c3f651f5a..bc57baf710 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UploadField.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UploadField.ts @@ -1,8 +1,8 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; // TODO: We won't include momentjs anymore so we need to find a way to handle date formats -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'File upload', alias: 'Umbraco.UploadField', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UserPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UserPicker.ts index 0e0d316c2f..0a09990d13 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UserPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/Umbraco.UserPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'User Picker', alias: 'Umbraco.UserPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/manifests.ts index a9409bf301..3a49b14704 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/models/manifests.ts @@ -25,14 +25,14 @@ import { manifest as slider } from './Umbraco.Slider.js'; import { manifest as tags } from './Umbraco.Tags.js'; import { manifest as textArea } from './Umbraco.TextArea.js'; import { manifest as textBox } from './Umbraco.TextBox.js'; -import { manifest as tinyMCE } from './Umbraco.TinyMCE.js'; +import { manifest as richText } from './Umbraco.RichText.js'; import { manifest as trueFalse } from './Umbraco.TrueFalse.js'; import { manifest as uploadField } from './Umbraco.UploadField.js'; import { manifest as userPicker } from './Umbraco.UserPicker.js'; -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ +export const manifests: Array = [ blockGrid, blockList, checkboxList, @@ -60,7 +60,7 @@ export const manifests: Array = [ tags, textArea, textBox, - tinyMCE, + richText, trueFalse, uploadField, userPicker, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/block-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/block-configuration/manifests.ts index 43286756d4..08c0612745 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/block-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/block-configuration/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-block-grid-block-configuration.element.js'), meta: { label: 'Block Grid Block Configuration', - propertyEditorAlias: 'Umbraco.BlockGrid.BlockConfiguration', + propertyEditorSchemaAlias: 'Umbraco.BlockGrid.BlockConfiguration', icon: 'umb:autofill', group: 'blocks', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/group-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/group-configuration/manifests.ts index d7c9bd0209..30014646ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/group-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/group-configuration/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-block-grid-group-configuration.element.js'), meta: { label: 'Block Grid Group Configuration', - propertyEditorAlias: 'Umbraco.BlockGrid.GroupConfiguration', + propertyEditorSchemaAlias: 'Umbraco.BlockGrid.GroupConfiguration', icon: 'umb:autofill', group: 'blocks', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/stylesheet-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/stylesheet-picker/manifests.ts index 340b29287d..ada6070970 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/stylesheet-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/config/stylesheet-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-block-grid-stylesheet-picker.element.js'), meta: { label: 'Block Grid Stylesheet Picker', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'blocks', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/manifests.ts index 78e2890e1c..590ecb2a48 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-grid/manifests.ts @@ -10,7 +10,7 @@ const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-block-grid.element.js'), meta: { label: 'Block Grid', - propertyEditorAlias: 'Umbraco.BlockGrid', + propertyEditorSchemaAlias: 'Umbraco.BlockGrid', icon: 'umb:icon-layout', group: 'richContent', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/config/block-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/config/block-configuration/manifests.ts index 8580adc8fe..17f2bd0d65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/config/block-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/config/block-configuration/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-block-list-block-configuration.element.js'), meta: { label: 'Block List Block Configuration', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/manifests.ts index f8a87af670..49a75bd072 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/block-list/manifests.ts @@ -8,7 +8,7 @@ const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-block-list.element.js'), meta: { label: 'Block List', - propertyEditorAlias: 'Umbraco.BlockList', + propertyEditorSchemaAlias: 'Umbraco.BlockList', icon: 'umb:thumbnail-list', group: 'lists', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/checkbox-list/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/checkbox-list/manifests.ts index df2ad411a2..b1d1349434 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/checkbox-list/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/checkbox-list/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-checkbox-list.element.js'), meta: { label: 'Checkbox List', - propertyEditorAlias: 'Umbraco.CheckboxList', + propertyEditorSchemaAlias: 'Umbraco.CheckboxList', icon: 'umb:bulleted-list', group: 'lists', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/bulk-action-permissions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/bulk-action-permissions/manifests.ts index aa9ba88bcc..c9062bb8cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/bulk-action-permissions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/bulk-action-permissions/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-collection-view-bulk-action-permissions.element.js'), meta: { label: 'Collection View Bulk Action Permissions', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'lists', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/column-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/column-configuration/manifests.ts index 971d393ac9..929d7f19da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/column-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/column-configuration/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-collection-view-column-configuration.element.js'), meta: { label: 'Collection View Column Configuration', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'lists', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/layout-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/layout-configuration/manifests.ts index c24fa89c2c..6b088e9ffe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/layout-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/layout-configuration/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-collection-view-layout-configuration.element.js'), meta: { label: 'Collection View Layout Configuration', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'lists', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/order-by/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/order-by/manifests.ts index 9bd87faddb..711d1cd002 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/order-by/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/config/order-by/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-collection-view-order-by.element.js'), meta: { label: 'Collection View Order By', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'lists', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/manifests.ts index 99f20c15d9..288c405d0d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/collection-view/manifests.ts @@ -11,7 +11,7 @@ const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-collection-view.element.js'), meta: { label: 'Collection View', - propertyEditorAlias: 'Umbraco.ListView', + propertyEditorSchemaAlias: 'Umbraco.ListView', icon: 'umb:bulleted-list', group: 'lists', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/color-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/color-picker/manifests.ts index d64117c7dc..b0b6db8878 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/color-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/color-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-color-picker.element.js'), meta: { label: 'Color Picker', - propertyEditorAlias: 'Umbraco.ColorPicker', + propertyEditorSchemaAlias: 'Umbraco.ColorPicker', icon: 'umb:colorpicker', group: 'pickers', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/date-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/date-picker/manifests.ts index c967960016..35cfecc8ac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/date-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/date-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-date-picker.element.js'), meta: { label: 'Date Picker', - propertyEditorAlias: 'Umbraco.DateTime', + propertyEditorSchemaAlias: 'Umbraco.DateTime', icon: 'umb:time', group: 'pickers', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/dropdown/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/dropdown/manifests.ts index 39018f8b2f..230e1e8588 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/dropdown/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/dropdown/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-dropdown.element.js'), meta: { label: 'Dropdown', - propertyEditorAlias: 'Umbraco.Dropdown', + propertyEditorSchemaAlias: 'Umbraco.Dropdown', icon: 'umb:time', group: 'pickers', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/eye-dropper/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/eye-dropper/manifests.ts index 03ac68fd4a..22729116a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/eye-dropper/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/eye-dropper/manifests.ts @@ -9,7 +9,7 @@ export const manifest: ManifestPropertyEditorUi = { label: 'Eye Dropper Color Picker', icon: 'umb:colorpicker', group: 'pickers', - propertyEditorAlias: 'Umbraco.ColorPicker.EyeDropper', + propertyEditorSchemaAlias: 'Umbraco.ColorPicker.EyeDropper', settings: { properties: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/icon-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/icon-picker/manifests.ts index bfdfe09949..f0f315a848 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/icon-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/icon-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-icon-picker.element.js'), meta: { label: 'Icon Picker', - propertyEditorAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', icon: 'umb:autofill', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-cropper/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-cropper/manifests.ts index 8e47b04edf..fa139e153d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-cropper/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-cropper/manifests.ts @@ -9,6 +9,6 @@ export const manifest: ManifestPropertyEditorUi = { label: 'Image Cropper', icon: 'umb:colorpicker', group: 'pickers', - propertyEditorAlias: 'Umbraco.ImageCropper', + propertyEditorSchemaAlias: 'Umbraco.ImageCropper', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-crops-configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-crops-configuration/manifests.ts index 8598edfe63..43e63c422d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-crops-configuration/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/image-crops-configuration/manifests.ts @@ -9,6 +9,6 @@ export const manifest: ManifestPropertyEditorUi = { label: 'Image Crops Configuration', icon: 'umb:autofill', group: 'common', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/label/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/label/manifests.ts index a342425827..112bf5bf55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/label/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/label/manifests.ts @@ -9,6 +9,6 @@ export const manifest: ManifestPropertyEditorUi = { label: 'Label', icon: 'umb:readonly', group: 'pickers', - propertyEditorAlias: 'Umbraco.Label', + propertyEditorSchemaAlias: 'Umbraco.Label', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/manifests.ts index 8c120ed5fe..bc9e65ecd1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/manifests.ts @@ -74,7 +74,7 @@ export const manifests: Array = [ label: 'Number', icon: 'umb:autofill', group: 'common', - propertyEditorAlias: 'Umbraco.Integer', + propertyEditorSchemaAlias: 'Umbraco.Integer', }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/markdown-editor/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/markdown-editor/manifests.ts index 8fb1db4358..64217e33dc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/markdown-editor/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/markdown-editor/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-markdown-editor.element.js'), meta: { label: 'Markdown Editor', - propertyEditorAlias: 'Umbraco.MarkdownEditor', + propertyEditorSchemaAlias: 'Umbraco.MarkdownEditor', icon: 'umb:code', group: 'pickers', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/media-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/media-picker/manifests.ts index 28d621cc27..3bf6b01f36 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/media-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/media-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-media-picker.element.js'), meta: { label: 'Media Picker', - propertyEditorAlias: 'Umbraco.MediaPicker3', + propertyEditorSchemaAlias: 'Umbraco.MediaPicker3', icon: 'umb:picture', group: 'pickers', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-group-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-group-picker/manifests.ts index 199b601fcc..f52da3c85f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-group-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-group-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-member-group-picker.element.js'), meta: { label: 'Member Group Picker', - propertyEditorAlias: 'Umbraco.MemberGroupPicker', + propertyEditorSchemaAlias: 'Umbraco.MemberGroupPicker', icon: 'umb:users-alt', group: 'people', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-picker/manifests.ts index 18394c5e32..7b35f57061 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/member-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-member-picker.element.js'), meta: { label: 'Member Picker', - propertyEditorAlias: 'Umbraco.MemberPicker', + propertyEditorSchemaAlias: 'Umbraco.MemberPicker', icon: 'umb:user', group: 'people', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multi-url-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multi-url-picker/manifests.ts index 1844b8e0d8..d06b22fc77 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multi-url-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multi-url-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-multi-url-picker.element.js'), meta: { label: 'Multi URL Picker', - propertyEditorAlias: 'Umbraco.MultiUrlPicker', + propertyEditorSchemaAlias: 'Umbraco.MultiUrlPicker', icon: 'umb:link', group: 'pickers', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multiple-text-string/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multiple-text-string/manifests.ts index 86db9a7d50..0f8f900a83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multiple-text-string/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/multiple-text-string/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-multiple-text-string.element.js'), meta: { label: 'Multiple Text String', - propertyEditorAlias: 'Umbraco.MultipleTextString', + propertyEditorSchemaAlias: 'Umbraco.MultipleTextString', icon: 'umb:ordered-list', group: '', supportsReadOnly: true, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number-range/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number-range/manifests.ts index 970f463008..42f4419e19 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number-range/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number-range/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-number-range.element.js'), meta: { label: 'Number Range', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number/manifests.ts index f401fcb8cc..1bc94f246a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/number/manifests.ts @@ -15,7 +15,7 @@ export const manifests: Array = [ loader: () => import('./property-editor-ui-number.element.js'), meta: { label: 'Integer', - propertyEditorAlias: 'Umbraco.Integer', + propertyEditorSchemaAlias: 'Umbraco.Integer', icon: 'umb:autofill', group: 'common', settings: { @@ -36,7 +36,7 @@ export const manifests: Array = [ loader: () => import('./property-editor-ui-number.element.js'), meta: { label: 'Decimal', - propertyEditorAlias: 'Umbraco.Decimal', + propertyEditorSchemaAlias: 'Umbraco.Decimal', icon: 'umb:autofill', group: 'common', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/order-direction/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/order-direction/manifests.ts index ee17704ef3..315121c8e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/order-direction/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/order-direction/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-order-direction.element.js'), meta: { label: 'Order Direction', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/overlay-size/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/overlay-size/manifests.ts index 0300271f0e..5e85579e45 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/overlay-size/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/overlay-size/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-overlay-size.element.js'), meta: { label: 'Overlay Size', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:document', group: '', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/radio-button-list/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/radio-button-list/manifests.ts index 28637d28f6..459d5c9695 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/radio-button-list/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/radio-button-list/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-radio-button-list.element.js'), meta: { label: 'Radio Button List', - propertyEditorAlias: 'Umbraco.RadioButtonList', + propertyEditorSchemaAlias: 'Umbraco.RadioButtonList', icon: 'umb:target', group: 'lists', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/slider/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/slider/manifests.ts index 662200e610..c5b4a66f93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/slider/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/slider/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-slider.element.js'), meta: { label: 'Slider', - propertyEditorAlias: 'Umbraco.Slider', + propertyEditorSchemaAlias: 'Umbraco.Slider', icon: 'umb:navigation-horizontal', group: 'common', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/text-box/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/text-box/manifests.ts index 6059b3a2a7..6084d0bb9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/text-box/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/text-box/manifests.ts @@ -16,7 +16,7 @@ export const manifests: Array = [ loader: () => import('./property-editor-ui-text-box.element.js'), meta: { label: 'Text Box', - propertyEditorAlias: 'Umbraco.TextBox', + propertyEditorSchemaAlias: 'Umbraco.TextBox', icon: 'umb:autofill', group: 'common', settings: { @@ -37,7 +37,7 @@ export const manifests: Array = [ loader: () => import('./property-editor-ui-text-box.element.js'), meta: { label: 'Email', - propertyEditorAlias: 'Umbraco.EmailAddress', + propertyEditorSchemaAlias: 'Umbraco.EmailAddress', icon: 'umb:message', group: 'common', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/textarea/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/textarea/manifests.ts index 8d63aaf6a0..bcf042cccf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/textarea/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/textarea/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-textarea.element.js'), meta: { label: 'Text Area', - propertyEditorAlias: 'Umbraco.TextArea', + propertyEditorSchemaAlias: 'Umbraco.TextArea', icon: 'umb:edit', group: 'common', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/manifests.ts deleted file mode 100644 index 16f0d58dab..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/manifests.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; - -export const manifest: ManifestPropertyEditorUi = { - type: 'propertyEditorUi', - alias: 'Umb.PropertyEditorUi.TinyMCE.Config', - name: 'Tiny MCE Configuration Property Editor UI', - loader: () => import('./property-editor-ui-tiny-mce-configuration.element.js'), - meta: { - label: 'Rich Text Editor Configuration', - propertyEditorAlias: 'Umbraco.TinyMCE.Configuration', - icon: 'umb:autofill', - group: 'common', - }, -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.element.ts deleted file mode 100644 index 94b80a7233..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.element.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; -import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; - -/** - * @element umb-property-editor-ui-tiny-mce-configuration - */ -@customElement('umb-property-editor-ui-tiny-mce-configuration') -export class UmbPropertyEditorUITinyMceConfigurationElement extends UmbLitElement { - @property() - value = ''; - - @property({ type: Array, attribute: false }) - public config = []; - - render() { - return html`
umb-property-editor-ui-tiny-mce-configuration
`; - } - - static styles = [UUITextStyles]; -} - -export default UmbPropertyEditorUITinyMceConfigurationElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-property-editor-ui-tiny-mce-configuration': UmbPropertyEditorUITinyMceConfigurationElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.stories.ts deleted file mode 100644 index b61d291165..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.stories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Meta, Story } from '@storybook/web-components'; -import type { UmbPropertyEditorUITinyMceConfigurationElement } from './property-editor-ui-tiny-mce-configuration.element.js'; -import { html } from '@umbraco-cms/backoffice/external/lit'; - -import './property-editor-ui-tiny-mce-configuration.element.js'; - -export default { - title: 'Property Editor UIs/Tiny Mce Configuration', - component: 'umb-property-editor-ui-tiny-mce-configuration', - id: 'umb-property-editor-ui-tiny-mce-configuration', -} as Meta; - -export const AAAOverview: Story = () => - html``; -AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.test.ts deleted file mode 100644 index eeb568e876..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/configuration/property-editor-ui-tiny-mce-configuration.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect, fixture, html } from '@open-wc/testing'; -import { UmbPropertyEditorUITinyMceConfigurationElement } from './property-editor-ui-tiny-mce-configuration.element.js'; -import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; - -describe('UmbPropertyEditorUITinyMceConfigurationElement', () => { - let element: UmbPropertyEditorUITinyMceConfigurationElement; - - beforeEach(async () => { - element = await fixture( - html` ` - ); - }); - - it('is defined with its own instance', () => { - expect(element).to.be.instanceOf(UmbPropertyEditorUITinyMceConfigurationElement); - }); - - it('passes the a11y audit', async () => { - await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); - }); -}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts new file mode 100644 index 0000000000..a647a425a9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts @@ -0,0 +1,27 @@ +import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +/** + * @element umb-property-editor-ui-tiny-mce-dimensions-configuration + */ +@customElement('umb-property-editor-ui-tiny-mce-dimensions-configuration') +export class UmbPropertyEditorUITinyMceDimensionsConfigurationElement extends UmbLitElement { + @property({ type: Object }) + value: { width?: number; height?: number } = {}; + + render() { + return html` x + pixels`; + } + + static styles = [UUITextStyles]; +} + +export default UmbPropertyEditorUITinyMceDimensionsConfigurationElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-tiny-mce-dimensions-configuration': UmbPropertyEditorUITinyMceDimensionsConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts new file mode 100644 index 0000000000..ffc191b709 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts @@ -0,0 +1,20 @@ +import { Meta } from '@storybook/web-components'; +import './property-editor-ui-tiny-mce-dimensions-configuration.element.js'; +import { umbDataTypeData } from '../../../../../../../mocks/data/data-type.data.js'; +import { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +const dataTypeData = umbDataTypeData.getById('dt-richTextEditor') as DataTypeResponseModel; + +export default { + title: 'Property Editor UIs/Tiny Mce Dimensions Configuration', + component: 'umb-property-eDitor-ui-tiny-mce-dimensions-configuration', + id: 'umb-property-editor-ui-tiny-mce-dimensions-configuration', +} as Meta; + +export const AAAOverview = ({ value }: any) => + html``; +AAAOverview.storyName = 'Overview'; +AAAOverview.args = { + value: dataTypeData?.values?.find((x) => x.alias === 'dimensions')?.value, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts new file mode 100644 index 0000000000..9e0924d0db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts @@ -0,0 +1,21 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbPropertyEditorUITinyMceDimensionsConfigurationElement } from './property-editor-ui-tiny-mce-dimensions-configuration.element.js'; +import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUITinyMceDimensionsConfigurationElement', () => { + let element: UmbPropertyEditorUITinyMceDimensionsConfigurationElement; + + beforeEach(async () => { + element = await fixture( + html` ` + ); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUITinyMceDimensionsConfigurationElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/manifests.ts new file mode 100644 index 0000000000..f22b4ef14d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/manifests.ts @@ -0,0 +1,54 @@ +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; + +const configurationManifests: Array = [ + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUI.TinyMCE.ToolbarConfiguration', + name: 'TinyMCE Toolbar Property Editor UI', + loader: () => import('./toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.js'), + meta: { + label: 'TinyMCE Toolbar Configuration', + propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration', + icon: 'umb:autofill', + group: 'common', + }, + }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUI.TinyMCE.StylesheetsConfiguration', + name: 'TinyMCE Stylesheets Property Editor UI', + loader: () => import('./stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.js'), + meta: { + label: 'TinyMCE Stylesheets Configuration', + propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration', + icon: 'umb:autofill', + group: 'common', + }, + }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUI.TinyMCE.DimensionsConfiguration', + name: 'TinyMCE Dimensions Property Editor UI', + loader: () => import('./dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.js'), + meta: { + label: 'TinyMCE Dimensions Configuration', + propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration', + icon: 'umb:autofill', + group: 'common', + }, + }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUI.TinyMCE.MaxImageSizeConfiguration', + name: 'TinyMCE Max Image Size Property Editor UI', + loader: () => import('./max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.element.js'), + meta: { + label: 'TinyMCE Max Image Size Configuration', + propertyEditorSchemaAlias: 'Umbraco.TinyMCE.Configuration', + icon: 'umb:autofill', + group: 'common', + }, + }, +]; + +export const manifests = [...configurationManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.element.ts new file mode 100644 index 0000000000..e6e6f097e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.element.ts @@ -0,0 +1,27 @@ +import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +/** + * @element umb-property-editor-ui-tiny-mce-maximagesize-configuration + */ +@customElement('umb-property-editor-ui-tiny-mce-maximagesize-configuration') +export class UmbPropertyEditorUITinyMceMaxImageSizeConfigurationElement extends UmbLitElement { + + @property() + value?: number; + + render() { + return html``; + } + + static styles = [UUITextStyles]; +} + +export default UmbPropertyEditorUITinyMceMaxImageSizeConfigurationElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-tiny-mce-maximagesize-configuration': UmbPropertyEditorUITinyMceMaxImageSizeConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.stories.ts new file mode 100644 index 0000000000..5996547659 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.stories.ts @@ -0,0 +1,22 @@ +import { Meta } from '@storybook/web-components'; +import { umbDataTypeData } from '../../../../../../../mocks/data/data-type.data.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +import './property-editor-ui-tiny-mce-maximagesize-configuration.element.js'; +import { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +const dataTypeData = umbDataTypeData.getById('dt-richTextEditor') as DataTypeResponseModel; + +export default { + title: 'Property Editor UIs/Tiny Mce Max Image Size Configuration', + component: 'umb-property-editor-ui-tiny-mce-maximagesize-configuration', + id: 'umb-property-editor-ui-tiny-mce-maximagesize-configuration', +} as Meta; + +export const AAAOverview = ({ value }: any) => + html``; +AAAOverview.storyName = 'Overview'; +AAAOverview.args = { + value: dataTypeData?.values?.find((x) => x.alias === 'maxImageSize')?.value, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.test.ts new file mode 100644 index 0000000000..c5d332a134 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/max-image-size/property-editor-ui-tiny-mce-maximagesize-configuration.test.ts @@ -0,0 +1,21 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbPropertyEditorUITinyMceMaxImageSizeConfigurationElement } from './property-editor-ui-tiny-mce-maximagesize-configuration.element.js'; +import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUITinyMceMaxImSizeConfigurationElement', () => { + let element: UmbPropertyEditorUITinyMceMaxImageSizeConfigurationElement; + + beforeEach(async () => { + element = await fixture( + html` ` + ); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUITinyMceMaxImageSizeConfigurationElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts new file mode 100644 index 0000000000..c4d4fa732f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts @@ -0,0 +1,40 @@ +import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +/** + * @element umb-property-editor-ui-tiny-mce-stylesheets-configuration + */ +@customElement('umb-property-editor-ui-tiny-mce-stylesheets-configuration') +export class UmbPropertyEditorUITinyMceStylesheetsConfigurationElement extends UmbLitElement { + @property() + value: string[] = []; + + @property({ type: Array, attribute: false }) + public config = []; + + render() { + return html`
    + ${this.value?.map((v) => html`
  • ${v}
  • `)} +
`; + } + + static styles = [ + UUITextStyles, + css` + ul { + list-style: none; + padding: 0; + margin: 0; + } + `, + ]; +} + +export default UmbPropertyEditorUITinyMceStylesheetsConfigurationElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-tiny-mce-stylesheets-configuration': UmbPropertyEditorUITinyMceStylesheetsConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts new file mode 100644 index 0000000000..759a1e255c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts @@ -0,0 +1,21 @@ +import { Meta } from '@storybook/web-components'; +import { umbDataTypeData } from '../../../../../../../mocks/data/data-type.data.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +import './property-editor-ui-tiny-mce-stylesheets-configuration.element.js'; +import { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +const dataTypeData = umbDataTypeData.getById('dt-richTextEditor') as DataTypeResponseModel; + +export default { + title: 'Property Editor UIs/Tiny Mce Stylesheets Configuration', + component: 'umb-property-editor-ui-tiny-mce-stylesheets-configuration', + id: 'umb-property-editor-ui-tiny-mce-stylesheets-configuration', +} as Meta; + +export const AAAOverview = ({ value }: any) => + html``; +AAAOverview.storyName = 'Overview'; +AAAOverview.args = { + value: dataTypeData?.values?.find(x => x.alias === 'stylesheets')?.value ?? [] +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts new file mode 100644 index 0000000000..aeb5a5df8a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts @@ -0,0 +1,21 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbPropertyEditorUITinyMceStylesheetsConfigurationElement } from './property-editor-ui-tiny-mce-stylesheets-configuration.element.js'; +import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUITinyMceStylesheetsConfigurationElement', () => { + let element: UmbPropertyEditorUITinyMceStylesheetsConfigurationElement; + + beforeEach(async () => { + element = await fixture( + html` ` + ); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUITinyMceStylesheetsConfigurationElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts new file mode 100644 index 0000000000..cf5efd2457 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts @@ -0,0 +1,143 @@ +import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { customElement, css, html, property, map, state, PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { UmbPropertyEditorExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { UmbDataTypePropertyCollection } from '@umbraco-cms/backoffice/components'; +import { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; + +const tinyIconSet = tinymce.default?.IconManager.get('default'); + +type ToolbarConfig = { + alias: string; + label: string; + icon?: string; + selected: boolean; +}; + +/** + * @element umb-property-editor-ui-tiny-mce-toolbar-configuration + */ +@customElement('umb-property-editor-ui-tiny-mce-toolbar-configuration') +export class UmbPropertyEditorUITinyMceToolbarConfigurationElement + extends UmbLitElement + implements UmbPropertyEditorExtensionElement +{ + @property() + set value(value: string | string[] | null) { + if (!value) return; + + if (typeof value === 'string') { + this.#selectedValues = value.split(',').filter(x => x.length > 0); + } else if (Array.isArray(value)) { + this.#selectedValues = value; + } else { + this.#selectedValues = []; + return; + } + + // Migrations + if (this.#selectedValues.includes('ace')) { + this.#selectedValues = this.#selectedValues.filter((v) => v !== 'ace'); + this.#selectedValues.push('sourcecode'); + } + + this._toolbarConfig.forEach((v) => { + v.selected = this.#selectedValues.includes(v.alias); + }); + } + + get value(): string[] { + return this.#selectedValues; + } + + @property({ type: Array, attribute: false }) + config?: UmbDataTypePropertyCollection; + + @state() + private _toolbarConfig: ToolbarConfig[] = []; + + #selectedValues: string[] = []; + + protected async firstUpdated(_changedProperties: PropertyValueMap) { + super.firstUpdated(_changedProperties); + + this.config?.getValueByAlias('toolbar')?.forEach((v) => { + this._toolbarConfig.push({ + ...v, + selected: this.value.includes(v.alias), + }); + }); + + await this.getToolbarPlugins(); + + this.requestUpdate('_toolbarConfig'); + } + + private async getToolbarPlugins(): Promise { + // Get all the toolbar plugins + const plugin$ = umbExtensionsRegistry.extensionsOfType('tinyMcePlugin'); + + const plugins = await firstValueFrom(plugin$); + + plugins.forEach((p) => { + // If the plugin has a toolbar, add it to the config + if (p.meta?.toolbar) { + p.meta.toolbar.forEach((t) => { + this._toolbarConfig.push({ + alias: t.alias, + label: t.label, + icon: t.icon ?? 'umb:autofill', + selected: this.value.includes(t.alias), + }); + }); + } + }); + } + + private onChange(event: CustomEvent) { + const checkbox = event.target as HTMLInputElement; + const alias = checkbox.value; + + if (checkbox.checked) { + this.value = [...this.value, alias]; + } else { + this.value = this.value.filter((v) => v !== alias); + } + + this.dispatchEvent(new CustomEvent('property-value-change')); + } + + render() { + return html`
    + ${map( + this._toolbarConfig, + (v) => html`
  • + + + ${v.label} + +
  • ` + )} +
`; + } + + static styles = [ + UUITextStyles, + css` + ul { + list-style: none; + padding: 0; + margin: 0; + } + `, + ]; +} + +export default UmbPropertyEditorUITinyMceToolbarConfigurationElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-tiny-mce-toolbar-configuration': UmbPropertyEditorUITinyMceToolbarConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts new file mode 100644 index 0000000000..7f6ed545ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts @@ -0,0 +1,23 @@ +import { Meta } from '@storybook/web-components'; +import { umbDataTypeData } from '../../../../../../../mocks/data/data-type.data.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +import './property-editor-ui-tiny-mce-toolbar-configuration.element.js'; +import { DataTypeResponseModel } from '@umbraco-cms/backoffice/backend-api'; + +const dataTypeData = umbDataTypeData.getById('dt-richTextEditor') as DataTypeResponseModel; + +export default { + title: 'Property Editor UIs/Tiny Mce Toolbar Configuration', + component: 'umb-property-editor-ui-tiny-mce-toolbar-configuration', + id: 'umb-property-editor-ui-tiny-mce-toolbar-configuration', +} as Meta; + +export const AAAOverview = ({ value }: any) => + html``; + + AAAOverview.storyName = 'Overview'; + + AAAOverview.args = { + value: dataTypeData?.values?.find(x => x.alias === 'toolbar')?.value ?? [] + } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts new file mode 100644 index 0000000000..e54b1a385a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/config/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts @@ -0,0 +1,23 @@ +import { expect, fixture, html } from '@open-wc/testing'; +import { UmbPropertyEditorUITinyMceToolbarConfigurationElement } from './property-editor-ui-tiny-mce-toolbar-configuration.element.js'; +import { defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUITinyMceToolbarConfigurationElement', () => { + let element: UmbPropertyEditorUITinyMceToolbarConfigurationElement; + + beforeEach(async () => { + element = await fixture( + html` + + ` + ); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUITinyMceToolbarConfigurationElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/manifests.ts index d099fecdd5..d2e5f27509 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/manifests.ts @@ -1,4 +1,4 @@ -import { manifest as configuration } from './config/configuration/manifests.js'; +import { manifests as configuration } from './config/manifests.js'; import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; const manifest: ManifestPropertyEditorUi = { @@ -8,19 +8,223 @@ const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-tiny-mce.element.js'), meta: { label: 'Rich Text Editor', - propertyEditorAlias: 'Umbraco.TinyMCE', + propertyEditorSchemaAlias: 'Umbraco.TinyMCE', icon: 'umb:browser-window', group: 'richText', settings: { properties: [ { - alias: 'editor', - label: 'Editor', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE.Configuration', + alias: 'toolbar', + label: 'Toolbar', + description: 'Pick the toolbar options that should be available when editing', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.ToolbarConfiguration', + config: [ + { + alias: 'toolbar', + value: [ + { + alias: 'undo', + label: 'Undo', + icon: 'undo', + }, + { + alias: 'redo', + label: 'Redo', + icon: 'redo', + }, + { + alias: 'cut', + label: 'Cut', + icon: 'cut', + }, + { + alias: 'copy', + label: 'Copy', + icon: 'copy', + }, + { + alias: 'paste', + label: 'Paste', + icon: 'paste', + }, + { + alias: 'styles', + label: 'Style select', + icon: 'permanent-pen', + }, + { + alias: 'fontname', + label: 'Font select', + icon: 'text-color', + }, + { + alias: 'fontsize', + label: 'Font size', + icon: 'text-color', + }, + { + alias: 'forecolor', + label: 'Text color', + icon: 'text-color', + }, + { + alias: 'backcolor', + label: 'Background color', + icon: 'highlight-bg-color', + }, + { + alias: 'blockquote', + label: 'Blockquote', + icon: 'quote', + }, + { + alias: 'formatblock', + label: 'Format block', + icon: 'format', + }, + { + alias: 'removeformat', + label: 'Remove format', + icon: 'remove-formatting', + }, + { + alias: 'bold', + label: 'Bold', + icon: 'bold', + }, + { + alias: 'italic', + label: 'Italic', + icon: 'italic', + }, + { + alias: 'underline', + label: 'Underline', + icon: 'underline', + }, + { + alias: 'strikethrough', + label: 'Strikethrough', + icon: 'strike-through', + }, + { + alias: 'alignleft', + label: 'Align left', + icon: 'align-left', + }, + { + alias: 'aligncenter', + label: 'Align center', + icon: 'align-center', + }, + { + alias: 'alignright', + label: 'Align right', + icon: 'align-right', + }, + { + alias: 'alignjustify', + label: 'Justify justify', + icon: 'align-justify', + }, + { + alias: 'bullist', + label: 'Bullet list', + icon: 'unordered-list', + }, + { + alias: 'numlist', + label: 'Numbered list', + icon: 'ordered-list', + }, + { + alias: 'outdent', + label: 'Outdent', + icon: 'outdent', + }, + { + alias: 'indent', + label: 'Indent', + icon: 'indent', + }, + { + alias: 'anchor', + label: 'Anchor', + icon: 'bookmark', + }, + { + alias: 'table', + label: 'Table', + icon: 'table', + }, + { + alias: 'hr', + label: 'Horizontal rule', + icon: 'horizontal-rule', + }, + { + alias: 'subscript', + label: 'Subscript', + icon: 'subscript', + }, + { + alias: 'superscript', + label: 'Superscript', + icon: 'superscript', + }, + { + alias: 'charmap', + label: 'Character map', + icon: 'insert-character', + }, + { + alias: 'rtl', + label: 'Right to left', + icon: 'rtl', + }, + { + alias: 'ltr', + label: 'Left to right', + icon: 'ltr', + }, + ], + }, + ], + }, + { + alias: 'stylesheets', + label: 'Stylesheets', + description: 'Pick the stylesheets whose editor styles should be available when editing', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.StylesheetsConfiguration', + }, + { + alias: 'dimensions', + label: 'Dimensions', + description: 'Set the editor dimensions', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.DimensionsConfiguration', + }, + { + alias: 'maxImageSize', + label: 'Maximum size for inserted images', + description: 'Maximum width or height - enter 0 to disable resizing', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.MaxImageSizeConfiguration', + }, + { + alias: 'mode', + label: 'Mode', + description: 'Select the mode for the editor', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Dropdown', + config: [ + { + alias: 'items', + value: ['Classic', 'Inline'], + }, + ], }, { alias: 'overlaySize', label: 'Overlay Size', + description: 'Select the width of the overlay (link picker)', propertyEditorUiAlias: 'Umb.PropertyEditorUi.OverlaySize', }, { @@ -33,6 +237,4 @@ const manifest: ManifestPropertyEditorUi = { }, }; -const config = [configuration]; - -export const manifests = [manifest, ...config]; +export const manifests = [manifest, ...configuration]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/manifests.ts new file mode 100644 index 0000000000..be1274b4bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/manifests.ts @@ -0,0 +1,86 @@ +import type { ManifestTinyMcePlugin } from '@umbraco-cms/backoffice/extension-registry'; + +const pluginManifests: Array = [ + { + type: 'tinyMcePlugin', + alias: 'Umb.TinyMcePlugin.CodeEditor', + name: 'Code Editor TinyMCE Plugin', + loader: () => import('./tiny-mce-code-editor.plugin.js'), + meta: { + toolbar: [ + { + alias: 'sourcecode', + label: 'Source code editor', + icon: 'sourcecode', + }, + ], + }, + }, + { + type: 'tinyMcePlugin', + alias: 'Umb.TinyMcePlugin.LinkPicker', + name: 'Link Picker TinyMCE Plugin', + loader: () => import('./tiny-mce-linkpicker.plugin.js'), + meta: { + toolbar: [ + { + alias: 'link', + label: 'Insert/Edit link', + icon: 'link', + }, + { + alias: 'unlink', + label: 'Remove link', + icon: 'unlink', + }, + ], + }, + }, + { + type: 'tinyMcePlugin', + alias: 'Umb.TinyMcePlugin.MediaPicker', + name: 'Media Picker TinyMCE Plugin', + loader: () => import('./tiny-mce-mediapicker.plugin.js'), + meta: { + toolbar: [ + { + alias: 'umbmediapicker', + label: 'Image', + icon: 'image', + }, + ], + }, + }, + { + type: 'tinyMcePlugin', + alias: 'Umb.TinyMcePlugin.EmbeddedMedia', + name: 'Embedded Media TinyMCE Plugin', + loader: () => import('./tiny-mce-embeddedmedia.plugin.js'), + meta: { + toolbar: [ + { + alias: 'umbembeddialog', + label: 'Embed', + icon: 'embed', + }, + ], + }, + }, + { + type: 'tinyMcePlugin', + alias: 'Umb.TinyMcePlugin.MacroPicker', + name: 'Macro Picker TinyMCE Plugin', + loader: () => import('./tiny-mce-macropicker.plugin.js'), + meta: { + toolbar: [ + { + alias: 'umbmacro', + label: 'Macro', + icon: 'preferences', + }, + ], + }, + }, +]; + +export const manifests = [...pluginManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts new file mode 100644 index 0000000000..0a430604fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts @@ -0,0 +1,48 @@ +import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components'; +import { + UmbCodeEditorModalData, + UmbCodeEditorModalResult, + UMB_CODE_EDITOR_MODAL, + UmbModalManagerContext, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, +} from '@umbraco-cms/backoffice/modal'; + +export default class UmbTinyMceCodeEditorPlugin extends UmbTinyMcePluginBase { + #modalContext?: UmbModalManagerContext; + + constructor(args: TinyMcePluginArguments) { + super(args); + + this.host.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (modalContext) => { + this.#modalContext = modalContext; + }); + + this.editor.ui.registry.addButton('sourcecode', { + icon: 'sourcecode', + tooltip: 'View Source Code', + onAction: () => this.#showCodeEditor(), + }); + } + + async #showCodeEditor() { + const modalHandler = this.#modalContext?.open( + UMB_CODE_EDITOR_MODAL, + { + headline: 'Edit source code', + content: this.editor.getContent() ?? '', + language: 'html', + } + ); + + if (!modalHandler) return; + + const { content } = await modalHandler.onSubmit(); + if (!content) { + this.editor.resetContent(); + } else { + this.editor.setContent(content.toString()); + } + + this.editor.dispatch('Change'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts new file mode 100644 index 0000000000..cebd20e6a9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts @@ -0,0 +1,90 @@ +import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components'; +import { + UmbEmbeddedMediaModalData, + UmbEmbeddedMediaModalResult, + UMB_EMBEDDED_MEDIA_MODAL, + UmbModalManagerContext, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, +} from '@umbraco-cms/backoffice/modal'; + +export default class UmbTinyMceEmbeddedMediaPlugin extends UmbTinyMcePluginBase { + #modalContext?: UmbModalManagerContext; + + constructor(args: TinyMcePluginArguments) { + super(args); + + this.host.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (modalContext) => { + this.#modalContext = modalContext; + }); + + this.editor.ui.registry.addButton('umbembeddialog', { + icon: 'embed', + tooltip: 'Embed', + onAction: () => this.#onAction(), + }); + } + + #onAction() { + // Get the selected element + // Check nodename is a DIV and the claslist contains 'umb-embed-holder' + const selectedElm = this.editor.selection.getNode(); + + let modify: UmbEmbeddedMediaModalData = { + width: 360, + height: 240, + }; + + if (selectedElm.nodeName.toUpperCase() === 'DIV' && selectedElm.classList.contains('umb-embed-holder')) { + // See if we can go and get the attributes + const url = this.editor.dom.getAttrib(selectedElm, 'data-embed-url'); + const embedWidth = this.editor.dom.getAttrib(selectedElm, 'data-embed-width'); + const embedHeight = this.editor.dom.getAttrib(selectedElm, 'data-embed-height'); + const constrain = this.editor.dom.getAttrib(selectedElm, 'data-embed-constrain') === 'true'; + + modify = { + url, + constrain, + width: parseInt(embedWidth) || modify.width, + height: parseInt(embedHeight) || modify.height, + }; + } + + this.#showModal(selectedElm, modify); + } + + #insertInEditor(embed: UmbEmbeddedMediaModalResult, activeElement: HTMLElement) { + // Wrap HTML preview content here in a DIV with non-editable class of .mceNonEditable + // This turns it into a selectable/cutable block to move about + const wrapper = this.editor.dom.create( + 'div', + { + class: 'mceNonEditable umb-embed-holder', + 'data-embed-url': embed.url ?? '', + 'data-embed-height': embed.height, + 'data-embed-width': embed.width, + 'data-embed-constrain': embed.constrain ?? false, + contenteditable: false, + }, + embed.preview + ); + + // Only replace if activeElement is an Embed element. + if (activeElement?.nodeName.toUpperCase() === 'DIV' && activeElement.classList.contains('umb-embed-holder')) { + activeElement.replaceWith(wrapper); // directly replaces the html node + } else { + this.editor.selection.setNode(wrapper); + } + } + + async #showModal(selectedElm: HTMLElement, embeddedMediaModalData: UmbEmbeddedMediaModalData) { + const modalHandler = this.#modalContext?.open(UMB_EMBEDDED_MEDIA_MODAL, embeddedMediaModalData); + + if (!modalHandler) return; + + const result = await modalHandler.onSubmit(); + if (!result) return; + + this.#insertInEditor(result, selectedElm); + this.editor.dispatch('Change'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-linkpicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-linkpicker.plugin.ts new file mode 100644 index 0000000000..756772a637 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-linkpicker.plugin.ts @@ -0,0 +1,223 @@ +import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components'; +import { + UmbLinkPickerModalResult, + UMB_LINK_PICKER_MODAL, + UmbLinkPickerLink, + UmbModalManagerContext, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, +} from '@umbraco-cms/backoffice/modal'; + +type AnchorElementAttributes = { + href?: string | null; + title?: string | null; + target?: string | null; + 'data-anchor'?: string | null; + rel?: string | null; + text?: string; +}; + +export default class UmbTinyMceLinkPickerPlugin extends UmbTinyMcePluginBase { + #modalContext?: UmbModalManagerContext; + + #linkPickerData?: UmbLinkPickerModalResult; + + #anchorElement?: HTMLAnchorElement; + + constructor(args: TinyMcePluginArguments) { + super(args); + + this.host.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (modalContext) => { + this.#modalContext = modalContext; + }); + + // const editorEventSetupCallback = (buttonApi: { setEnabled: (state: boolean) => void }) => { + // const editorEventCallback = (eventApi: { element: Element}) => { + // buttonApi.setEnabled(eventApi.element.nodeName.toLowerCase() === 'a' && eventApi.element.hasAttribute('href')); + // }; + + // editor.on('NodeChange', editorEventCallback); + // return () => editor.off('NodeChange', editorEventCallback); + // }; + + args.editor.ui.registry.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + onAction: () => this.showDialog(), + }); + + args.editor.ui.registry.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + onAction: () => args.editor.execCommand('unlink'), + }); + } + + async showDialog() { + const selectedElm = this.editor.selection.getNode(); + this.#anchorElement = this.editor.dom.getParent(selectedElm, 'a[href]') as HTMLAnchorElement; + + const data: AnchorElementAttributes = { + text: this.#anchorElement + ? this.#anchorElement.innerText || (this.#anchorElement.textContent ?? '') + : this.editor.selection.getContent({ format: 'text' }), + href: this.#anchorElement?.getAttribute('href') ?? '', + target: this.#anchorElement?.target ?? '', + rel: this.#anchorElement?.rel ?? '', + }; + + if (selectedElm.nodeName === 'IMG') { + data.text = ' '; + } + + if (!this.#anchorElement) { + this.#openLinkPicker(); + return; + } + + //if we already have a link selected, we want to pass that data over to the dialog + const currentTarget: UmbLinkPickerLink = { + name: this.#anchorElement.title, + url: this.#anchorElement.getAttribute('href') ?? '', + target: this.#anchorElement.target, + }; + + // drop the lead char from the anchor text, if it has a value + const anchorVal = this.#anchorElement.dataset.anchor; + if (anchorVal) { + currentTarget.queryString = anchorVal.substring(1); + } + + if (currentTarget.url?.includes('localLink:')) { + currentTarget.udi = + currentTarget.url?.substring(currentTarget.url.indexOf(':') + 1, currentTarget.url.lastIndexOf('}')) ?? ''; + } + + this.#openLinkPicker(currentTarget); + } + + // TODO => get anchors to provide to link picker? + async #openLinkPicker(currentTarget?: UmbLinkPickerLink) { + const modalHandler = this.#modalContext?.open(UMB_LINK_PICKER_MODAL, { + config: { + ignoreUserStartNodes: this.configuration?.getValueByAlias('ignoreUserStartNodes') ?? false, + }, + link: currentTarget ?? {}, + index: null, + }); + + if (!modalHandler) return; + + const linkPickerData = await modalHandler.onSubmit(); + if (!linkPickerData) return; + + this.#linkPickerData = linkPickerData; + this.#updateLink(); + } + + //Create a json obj used to create the attributes for the tag + // TODO => where has rel gone? + #createElemAttributes() { + const a: AnchorElementAttributes = Object.assign({}, this.#linkPickerData?.link, { 'data-anchor': null }); + + // always need to map back to href for tinymce to render correctly + // do this first as checking querystring below may modify the href property + if (this.#linkPickerData?.link.url) { + a.href = this.#linkPickerData.link.url; + } + + if (this.#linkPickerData?.link.queryString?.startsWith('#')) { + a['data-anchor'] = this.#linkPickerData?.link.queryString; + a.href += this.#linkPickerData?.link.queryString; + } + + return a; + } + + #insertLink() { + if (this.#anchorElement) { + this.editor.dom.setAttribs(this.#anchorElement, this.#createElemAttributes()); + this.editor.selection.select(this.#anchorElement); + this.editor.execCommand('mceEndTyping'); + return; + } + + // If there is no selected content, we can't insert a link + // as TinyMCE needs selected content for this, so instead we + // create a new dom element and insert it, using the chosen + // link name as the content. + + if (this.editor.selection.getContent() !== '') { + this.editor.execCommand('CreateLink', false, this.#createElemAttributes()); + return; + } + + // Using the target url as a fallback, as href might be confusing with a local link + const linkContent = + typeof this.#linkPickerData?.link.name !== 'undefined' && this.#linkPickerData?.link.name !== '' + ? this.#linkPickerData?.link.name + : this.#linkPickerData?.link.url; + + // only insert if link has content + if (linkContent) { + const domElement = this.editor.dom.createHTML('a', this.#createElemAttributes(), linkContent); + this.editor.execCommand('mceInsertContent', false, domElement); + } + } + + #updateLink() { + // if an anchor exists, check that it is appropriately prefixed + if ( + this.#linkPickerData?.link.queryString && + !this.#linkPickerData.link.queryString.startsWith('?') && + !this.#linkPickerData.link.queryString.startsWith('#') + ) { + this.#linkPickerData.link.queryString = + (this.#linkPickerData.link.queryString.startsWith('=') ? '#' : '?') + this.#linkPickerData.link.queryString; + } + + // the href might be an external url, so check the value for an anchor/qs + // href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor + if (this.#linkPickerData && !this.#linkPickerData?.link.queryString) { + const urlParts = this.#linkPickerData?.link.url?.split(/(#|\?)/); + if (urlParts?.length === 3) { + this.#linkPickerData.link.url = urlParts[0]; + this.#linkPickerData.link.queryString = urlParts[1] + urlParts[2]; + } + } + + if (!this.#linkPickerData?.link.url && !this.#linkPickerData?.link.queryString) { + this.editor.execCommand('unlink'); + return; + } + + //if we have an id, it must be a locallink:id + if (this.#linkPickerData?.link.udi) { + this.#linkPickerData.link.url = '/{localLink:' + this.#linkPickerData.link.udi + '}'; + this.#insertLink(); + return; + } + + if (!this.#linkPickerData?.link.url) { + this.#linkPickerData.link.url = ''; + } + + // Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified + if ( + this.#linkPickerData?.link.url.includes('@') && + !this.#linkPickerData.link.url.includes('//') && + !this.#linkPickerData.link.url.includes(':') + ) { + // assume it's a mailto link + this.#linkPickerData.link.url = 'mailto:' + this.#linkPickerData?.link.url; + this.#insertLink(); + return; + } + + // Is www. prefixed + if (/^\s*www\./i.test(this.#linkPickerData?.link.url)) { + this.#linkPickerData.link.url = 'http://' + this.#linkPickerData.link.url; + } + + this.#insertLink(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-macropicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-macropicker.plugin.ts new file mode 100644 index 0000000000..b83a88fb25 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-macropicker.plugin.ts @@ -0,0 +1,209 @@ +import { MacroSyntaxData, UmbMacroService } from '@umbraco-cms/backoffice/macro'; +import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components'; +import { + UMB_CONFIRM_MODAL, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, + UmbModalManagerContext, +} from '@umbraco-cms/backoffice/modal'; +import { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; + +interface DialogData { + richTextEditor: boolean; + macroData?: MacroSyntaxData | null; + activeMacroElement?: HTMLElement; +} + +// TODO => This is a quick transplant of the existing macro plugin - needs to be finished, and need to +// determine how to replicate the existing macro service +export default class UmbTinyMceMacroPickerPlugin extends UmbTinyMcePluginBase { + #macroService = new UmbMacroService(); + + #modalContext?: UmbModalManagerContext; + + constructor(args: TinyMcePluginArguments) { + super(args); + + this.host.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (modalContext) => { + this.#modalContext = modalContext; + }); + + /** Adds custom rules for the macro plugin and custom serialization */ + this.editor.on('preInit', () => { + //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, + // otherwise the cleanup will strip it out + this.editor.serializer.addRules('div'); + + /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */ + this.editor.serializer.addNodeFilter('div', (nodes: Array) => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].attr('class') === 'umb-macro-holder' && nodes[i].parent?.name.toLowerCase() === 'p') { + nodes[i].parent?.unwrap(); + } + } + }); + }); + + /** when the contents load we need to find any macros declared and load in their content */ + this.editor.on('SetContent', () => { + //get all macro divs and load their content + this.editor.dom.select('.umb-macro-holder.mceNonEditable').forEach((macroElement: HTMLElement) => { + this.#loadMacroContent(macroElement as HTMLDivElement, null); + }); + }); + + /** Adds the button instance */ + this.editor.ui.registry.addButton('umbmacro', { + icon: 'preferences', + tooltip: 'Insert macro', + + /** The insert macro button click event handler */ + onAction: () => { + let dialogData: DialogData = { + //flag for use in rte so we only show macros flagged for the editor + richTextEditor: true, + }; + + //when we click we could have a macro already selected and in that case we'll want to edit the current parameters + //so we'll need to extract them and submit them to the dialog. + const activeMacroElement = this.#getRealMacroElem(); + if (activeMacroElement) { + //we have a macro selected so we'll need to parse it's alias and parameters + const comment = Array.from(activeMacroElement.childNodes).find((x) => x.nodeType === 8); + if (!comment) { + throw 'Cannot parse the current macro, the syntax in the editor is invalid'; + } + + const syntax = comment.textContent?.trim(); + const parsed = this.#macroService?.parseMacroSyntax(syntax); + + dialogData = { + richTextEditor: false, + macroData: parsed, + activeMacroElement, //pass the active element along so we can retrieve it later + }; + } + + this.#showMacroPicker(dialogData); + }, + }); + } + + /** loads in the macro content async from the server */ + #loadMacroContent(macroDiv?: HTMLDivElement, macroData?: MacroSyntaxData | null) { + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData && macroDiv) { + const comment = Array.from(macroDiv.childNodes).find((x) => x.nodeType === 8); + + if (!comment) { + throw 'Cannot parse the current macro, the syntax in the editor is invalid'; + } + + const syntax = comment.textContent?.trim(); + const parsed = this.#macroService?.parseMacroSyntax(syntax); + macroData = parsed; + } + + //show the throbber + macroDiv?.classList.add('loading'); + + // Add the contenteditable="false" attribute + // As just the CSS class of .mceNonEditable is not working by itself?! + macroDiv?.setAttribute('contenteditable', 'false'); + + // TODO => macro data service? + // const contentId = $routeParams.id; + + // //need to wrap in safe apply since this might be occuring outside of angular + // angularHelper.safeApply($rootScope, function () { + // tryExecuteAndNotify(this, macroResource + // .getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary)) + // .then(function (htmlResult) { + // $macroDiv.removeClass('loading'); + // htmlResult = htmlResult.trim(); + // if (htmlResult !== '') { + // const wasDirty = editor.isDirty(); + // const $ins = macroDiv?.querySelector('ins'); + // $ins.html(htmlResult); + // if (!wasDirty) { + // editor.undoManager.clear(); + // } + // } + // }); + // }); + } + + #insertInEditor(macroObject: MacroSyntaxData, activeMacroElement?: HTMLElement) { + //Important note: the TinyMce plugin "noneditable" is used here so that the macro cannot be edited, + // for this to work the mceNonEditable class needs to come last and we also need to use the attribute contenteditable = false + // (even though all the docs and examples say that is not necessary) + + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + const macroSyntaxComment = ``; + //create an id class for this element so we can re-select it after inserting + const uniqueId = 'umb-macro-' + this.editor.dom.uniqueId(); + let macroDiv = this.editor.dom.create( + 'div', + { + class: `umb-macro-holder ${macroObject.macroAlias} ${uniqueId} mceNonEditable`, + contenteditable: 'false', + }, + `${macroSyntaxComment}Macro alias: ${macroObject.macroAlias}` + ); + + //if there's an activeMacroElement then replace it, otherwise set the contents of the selected node + if (activeMacroElement) { + activeMacroElement.replaceWith(macroDiv); //directly replaces the html node + } else { + this.editor.selection.setNode(macroDiv); + } + + macroDiv = this.editor.dom.select('div.umb-macro-holder.' + uniqueId)[0] as HTMLDivElement; + this.editor.setDirty(true); + + //async load the macro content + this.#loadMacroContent(macroDiv, macroObject); + } + + /** + * Because the macro got wrapped in a P tag because of the way 'enter' works in older versions of Umbraco, this + * method will return the macro element if not wrapped in a p, or the p if the macro + * element is the only one inside of it even if we are deep inside an element inside the macro + */ + #getRealMacroElem() { + // Ask the editor for the currently selected element + const element = this.editor?.selection.getNode() as HTMLElement; + if (!element) { + return null; + } + + const e = element.closest('.umb-macro-holder'); + if (!e || e === null) return null; + + if (e.parentNode?.nodeName === 'P') { + //now check if we're the only element + if (element.parentNode?.childNodes.length === 1) { + return e.parentNode as HTMLElement; + } + } + + return e as HTMLElement; + } + + // TODO => depends on macro picker, which doesn't exist, just showing a generic modal for now + async #showMacroPicker(dialogData: DialogData) { + const modalHandler = this.#modalContext?.open(UMB_CONFIRM_MODAL, { + headline: 'Macro picker', + content: 'Yet to be implemented', + }); + + if (!modalHandler) return; + + const result = await modalHandler.onSubmit(); + if (!result) return; + + // TODO => object here should be the response from the modal + this.#insertInEditor({} as MacroSyntaxData, dialogData.activeMacroElement); + this.editor.dispatch('Change'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts new file mode 100644 index 0000000000..ea3d757abe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -0,0 +1,179 @@ +import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components'; +import { UmbMediaHelper } from '@umbraco-cms/backoffice/utils'; +import { + UMB_MEDIA_TREE_PICKER_MODAL, + UmbModalManagerContext, + UMB_MODAL_MANAGER_CONTEXT_TOKEN, +} from '@umbraco-cms/backoffice/modal'; +import { UMB_AUTH, UmbLoggedInUser } from '@umbraco-cms/backoffice/auth'; + +interface MediaPickerTargetData { + altText?: string; + url?: string; + caption?: string; + udi?: string; + id?: string; + tmpimg?: string; +} + +interface MediaPickerResultData { + id?: string; + src?: string; + alt?: string; + 'data-udi'?: string; + 'data-caption'?: string; +} + +export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { + #mediaHelper: UmbMediaHelper; + #currentUser?: UmbLoggedInUser; + #modalContext?: UmbModalManagerContext; + #auth?: typeof UMB_AUTH.TYPE; + + constructor(args: TinyMcePluginArguments) { + super(args); + + this.#mediaHelper = new UmbMediaHelper(); + + this.host.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (modalContext) => { + this.#modalContext = modalContext; + }); + + // TODO => this breaks tests. disabling for now + // will ignore user media start nodes + // this.host.consumeContext(UMB_AUTH, (instance) => { + // this.#auth = instance; + // this.#observeCurrentUser(); + // }); + + this.editor.ui.registry.addButton('umbmediapicker', { + icon: 'image', + tooltip: 'Media Picker', + //stateSelector: 'img[data-udi]', TODO => Investigate where stateselector has gone, or if it is still needed + onAction: () => this.#onAction(), + }); + } + + async #observeCurrentUser() { + if (!this.#auth) return; + + this.host.observe(this.#auth.currentUser, (currentUser) => (this.#currentUser = currentUser)); + } + + async #onAction() { + const selectedElm = this.editor.selection.getNode(); + let currentTarget: MediaPickerTargetData = {}; + + if (selectedElm.nodeName === 'IMG') { + const img = selectedElm as HTMLImageElement; + const hasUdi = img.hasAttribute('data-udi'); + const hasDataTmpImg = img.hasAttribute('data-tmpimg'); + + currentTarget = { + altText: img.alt, + url: img.src, + caption: img.dataset.caption, + }; + + if (hasUdi) { + currentTarget['udi'] = img.dataset.udi; + } else { + currentTarget['id'] = img.getAttribute('rel') ?? undefined; + } + + if (hasDataTmpImg) { + currentTarget['tmpimg'] = img.dataset.tmpimg; + } + } + + this.#showMediaPicker(currentTarget); + } + + async #showMediaPicker(currentTarget: MediaPickerTargetData) { + let startNodeId; + let startNodeIsVirtual; + + if (!this.configuration?.getByAlias('startNodeId')) { + if (this.configuration?.getValueByAlias('ignoreUserStartNodes') === true) { + startNodeId = -1; + startNodeIsVirtual = true; + } else { + startNodeId = this.#currentUser?.mediaStartNodeIds?.length !== 1 ? -1 : this.#currentUser?.mediaStartNodeIds[0]; + startNodeIsVirtual = this.#currentUser?.mediaStartNodeIds?.length !== 1; + } + } + + // TODO => startNodeId and startNodeIsVirtual do not exist on ContentTreeItemResponseModel + const modalHandler = this.#modalContext?.open(UMB_MEDIA_TREE_PICKER_MODAL, { + selection: currentTarget.udi ? [...currentTarget.udi] : [], + multiple: false, + //startNodeId, + //startNodeIsVirtual, + }); + + if (!modalHandler) return; + + const { selection } = await modalHandler.onSubmit(); + if (!selection.length) return; + + this.#insertInEditor(selection[0]); + this.editor.dispatch('Change'); + } + + // TODO => mediaPicker returns a UDI, so need to fetch it. Wait for backend CLI before implementing + async #insertInEditor(img: any) { + if (!img) return; + + // We need to create a NEW DOM element to insert + // 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. + const data: MediaPickerResultData = { + alt: img.altText || '', + src: img.url ? img.url : 'nothing.jpg', + id: '__mcenew', + 'data-udi': img.udi, + 'data-caption': img.caption, + }; + const newImage = this.editor.dom.createHTML('img', data as Record); + const parentElement = this.editor.selection.getNode().parentElement; + + if (img.caption && parentElement) { + const figCaption = this.editor.dom.createHTML('figcaption', {}, img.caption); + const combined = newImage + figCaption; + + if (parentElement.nodeName !== 'FIGURE') { + const fragment = this.editor.dom.createHTML('figure', {}, combined); + this.editor.selection.setContent(fragment); + } else { + parentElement.innerHTML = combined; + } + } else { + //if caption is removed, remove the figure element + if (parentElement?.nodeName === 'FIGURE' && parentElement.parentElement) { + parentElement.parentElement.innerHTML = newImage; + } else { + this.editor.selection.setContent(newImage); + } + } + + // Using settimeout to wait for a DoM-render, so we can find the new element by ID. + setTimeout(() => { + const imgElm = this.editor.dom.get('__mcenew') as HTMLImageElement; + if (!imgElm) return; + + this.editor.dom.setAttrib(imgElm, 'id', null); + + // When image is loaded we are ready to call sizeImageInEditor. + const onImageLoaded = () => { + this.#mediaHelper?.sizeImageInEditor(this.editor, imgElm, img.url); + this.editor.dispatch('Change'); + }; + + // Check if image already is loaded. + if (imgElm.complete === true) { + onImageLoaded(); + } else { + imgElm.onload = onImageLoaded; + } + }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts index d82ef18c6d..4222d3be56 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.element.ts @@ -1,5 +1,5 @@ -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui'; +import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbPropertyEditorExtensionElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbDataTypePropertyCollection } from '@umbraco-cms/backoffice/components'; @@ -9,14 +9,27 @@ import { UmbDataTypePropertyCollection } from '@umbraco-cms/backoffice/component */ @customElement('umb-property-editor-ui-tiny-mce') export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements UmbPropertyEditorExtensionElement { - @property() + + #configuration?: UmbDataTypePropertyCollection; + + @property({ type: String }) value = ''; - @property({ type: Array, attribute: false }) - public config = new UmbDataTypePropertyCollection(); + @property({ attribute: false }) + public set config(config: UmbDataTypePropertyCollection) { + this.#configuration = config; + } + + #onChange(event: InputEvent) { + this.value = (event.target as HTMLInputElement).value; + this.dispatchEvent(new CustomEvent('property-value-change')); + } render() { - return html`
umb-property-editor-ui-tiny-mce
`; + return html``; } static styles = [UUITextStyles]; @@ -28,4 +41,4 @@ declare global { interface HTMLElementTagNameMap { 'umb-property-editor-ui-tiny-mce': UmbPropertyEditorUITinyMceElement; } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.stories.ts index 5a0c79174f..cc623647e9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tiny-mce/property-editor-ui-tiny-mce.stories.ts @@ -1,15 +1,107 @@ -import { Meta, Story } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components'; import type { UmbPropertyEditorUITinyMceElement } from './property-editor-ui-tiny-mce.element.js'; -import { html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDataTypePropertyCollection } from '@umbraco-cms/backoffice/components'; import './property-editor-ui-tiny-mce.element.js'; -export default { +const config = new UmbDataTypePropertyCollection([ + { + alias: 'hideLabel', + value: true, + }, + { alias: 'dimensions', value: { height: 500 } }, + { alias: 'maxImageSize', value: 500 }, + { alias: 'ignoreUserStartNodes', value: false }, + { + alias: 'validElements', + value: + '+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],-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,video[*],audio[*],picture[*],source[*],canvas[*]', + }, + { alias: 'invalidElements', value: 'font' }, + { + alias: 'toolbar', + value: [ + 'sourcecode', + 'undo', + 'redo', + 'styles', + 'bold', + 'italic', + 'alignleft', + 'aligncenter', + 'alignright', + 'bullist', + 'numlist', + 'outdent', + 'indent', + 'link', + 'unlink', + 'anchor', + 'table', + 'umbmediapicker', + 'umbmacro', + 'umbembeddialog', + ], + }, + { + alias: 'plugins', + value: [ + { + name: 'anchor', + }, + { + name: 'charmap', + }, + { + name: 'table', + }, + { + name: 'lists', + }, + { + name: 'advlist', + }, + { + name: 'autolink', + }, + { + name: 'directionality', + }, + { + name: 'searchreplace', + }, + ], + }, +]); + +const meta: Meta = { title: 'Property Editor UIs/Tiny Mce', component: 'umb-property-editor-ui-tiny-mce', id: 'umb-property-editor-ui-tiny-mce', -} as Meta; + args: { + config: undefined, + value: ` +

TinyMCE

+

I am a default value for the TinyMCE text editor story.

+

+ TinyMCE documentation +

+

+ TinyMCE quick start guide +

+ Umbraco documentation +

+ `, + }, +}; -export const AAAOverview: Story = () => - html``; -AAAOverview.storyName = 'Overview'; +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const DefaultConfig: Story = { + args: { + config, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/toggle/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/toggle/manifests.ts index 011906092a..b7dc5d0b42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/toggle/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/toggle/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-toggle.element.js'), meta: { label: 'Toggle', - propertyEditorAlias: 'Umbraco.TrueFalse', + propertyEditorSchemaAlias: 'Umbraco.TrueFalse', icon: 'umb:checkbox', group: 'common', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/config/start-node/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/config/start-node/manifests.ts index f569ee5504..291f884edf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/config/start-node/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/config/start-node/manifests.ts @@ -9,6 +9,6 @@ export const manifest: ManifestPropertyEditorUi = { label: 'Tree Picker Start Node', icon: 'umb:page-add', group: 'pickers', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/manifests.ts index d50b78f9af..0553ae2034 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/tree-picker/manifests.ts @@ -10,7 +10,7 @@ const manifest: ManifestPropertyEditorUi = { label: 'Tree Picker', icon: 'umb:page-add', group: 'pickers', - propertyEditorAlias: 'Umbraco.MultiNodeTreePicker', + propertyEditorSchemaAlias: 'Umbraco.MultiNodeTreePicker', settings: { properties: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/upload-field/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/upload-field/manifests.ts index 3cd6347dc5..fcbc5f574b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/upload-field/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/upload-field/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-upload-field.element.js'), meta: { label: 'Upload Field', - propertyEditorAlias: 'Umbraco.UploadField', + propertyEditorSchemaAlias: 'Umbraco.UploadField', icon: 'umb:download-alt', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/user-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/user-picker/manifests.ts index 6fe35ba8de..2e1cc22dff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/user-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/user-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-user-picker.element.js'), meta: { label: 'User Picker', - propertyEditorAlias: 'Umbraco.UserPicker', + propertyEditorSchemaAlias: 'Umbraco.UserPicker', icon: 'umb:user', group: 'people', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/value-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/value-type/manifests.ts index dadee77196..40b01b6858 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/value-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editors/uis/value-type/manifests.ts @@ -9,6 +9,6 @@ export const manifest: ManifestPropertyEditorUi = { label: 'Value Type', icon: 'umb:autofill', group: 'common', - propertyEditorAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/Umbraco.ContentPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/Umbraco.ContentPicker.ts index 792d6b07fc..bdd3e10e93 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/Umbraco.ContentPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/Umbraco.ContentPicker.ts @@ -1,7 +1,7 @@ -import type { ManifestPropertyEditorModel } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: ManifestPropertyEditorModel = { - type: 'propertyEditorModel', +export const manifest: ManifestPropertyEditorSchema = { + type: 'propertyEditorSchema', name: 'Content Picker', alias: 'Umbraco.ContentPicker', meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/manifests.ts index 09417384fc..0e2a76844a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-document-picker.element.js'), meta: { label: 'Document Picker', - propertyEditorAlias: 'Umbraco.ContentPicker', + propertyEditorSchemaAlias: 'Umbraco.ContentPicker', icon: 'umb:document', group: 'common', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/media.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/media.repository.ts index 7a380fcd7b..db3a6f8f12 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/media.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/media.repository.ts @@ -22,6 +22,7 @@ export class UmbMediaRepository UmbDetailRepository { #host: UmbControllerHostElement; + #init; #treeSource: UmbTreeDataSource; #treeStore?: UmbMediaTreeStore; @@ -31,9 +32,6 @@ export class UmbMediaRepository #notificationContext?: UmbNotificationContext; - #initResolver?: () => void; - #initialized = false; - constructor(host: UmbControllerHostElement) { this.#host = host; @@ -41,32 +39,19 @@ export class UmbMediaRepository this.#treeSource = new UmbMediaTreeServerDataSource(this.#host); this.#detailDataSource = new UmbMediaDetailServerDataSource(this.#host); - new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { - this.#treeStore = instance; - this.#checkIfInitialized(); - }); + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_MEDIA_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + }).asPromise(), - new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => { - this.#store = instance; - this.#checkIfInitialized(); - }); + new UmbContextConsumerController(this.#host, UMB_MEDIA_STORE_CONTEXT_TOKEN, (instance) => { + this.#store = instance; + }).asPromise(), - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { - this.#notificationContext = instance; - this.#checkIfInitialized(); - }); - } - - // TODO: make a generic way to wait for initialization - #init = new Promise((resolve) => { - this.#initialized ? resolve() : (this.#initResolver = resolve); - }); - - #checkIfInitialized() { - if (this.#treeStore && this.#store && this.#notificationContext) { - this.#initialized = true; - this.#initResolver?.(); - } + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#notificationContext = instance; + }).asPromise(), + ]); } // TREE: diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/members/repository/member.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/members/members/repository/member.repository.ts index 126986e698..a64f4c9887 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/members/repository/member.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/members/repository/member.repository.ts @@ -1,7 +1,6 @@ import { UmbMemberTreeStore, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN } from './member.tree.store.js'; import { UmbMemberTreeServerDataSource } from './sources/member.tree.server.data.js'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import type { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; @@ -9,35 +8,18 @@ export class UmbMemberRepository implements UmbTreeRepository { #host: UmbControllerHostElement; #dataSource: UmbMemberTreeServerDataSource; #treeStore?: UmbMemberTreeStore; - #notificationContext?: UmbNotificationContext; - #initResolver?: () => void; - #initialized = false; + #init; constructor(host: UmbControllerHostElement) { this.#host = host; // TODO: figure out how spin up get the correct data source this.#dataSource = new UmbMemberTreeServerDataSource(this.#host); - new UmbContextConsumerController(this.#host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN, (instance) => { - this.#treeStore = instance; - this.#checkIfInitialized(); - }); - - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { - this.#notificationContext = instance; - this.#checkIfInitialized(); - }); - } - - #init = new Promise((resolve) => { - this.#initialized ? resolve() : (this.#initResolver = resolve); - }); - - #checkIfInitialized() { - if (this.#treeStore && this.#notificationContext) { - this.#initialized = true; - this.#initResolver?.(); - } + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_MEMBER_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#treeStore = instance; + }).asPromise(), + ]); } // TREE: diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.element.ts index d3e4de7b3e..10179c4fef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.element.ts @@ -29,7 +29,7 @@ export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) { if (dataType) { this.name = dataType.name ?? ''; this.propertyEditorUiAlias = dataType.propertyEditorUiAlias ?? ''; - this.propertyEditorModelAlias = dataType.propertyEditorAlias ?? ''; + this.propertyEditorSchemaAlias = dataType.propertyEditorAlias ?? ''; } }, 'dataType' @@ -51,7 +51,7 @@ export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) { * Property Editor Model Alias */ @state() - propertyEditorModelAlias = ''; + propertyEditorSchemaAlias = ''; protected renderDetail() { const details: string[] = []; @@ -63,8 +63,8 @@ export class UmbRefDataTypeElement extends UmbElementMixin(UUIRefNodeElement) { } /* // TODO: Revisit if its fine to leave this out: - if (this.propertyEditorModelAlias !== '') { - details.push(this.propertyEditorModelAlias); + if (this.propertyEditorSchemaAlias !== '') { + details.push(this.propertyEditorSchemaAlias); } else { details.push('Property Editor Model Missing'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.stories.ts index 379dead092..5d4987403f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/components/ref-data-type/ref-data-type.stories.ts @@ -15,7 +15,7 @@ export const Overview: Story = { args: { name: 'Custom Data Type', propertyEditorUiAlias: 'Umb.DataTypeInput.CustomUI', - propertyEditorModelAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', }, }; @@ -23,7 +23,7 @@ export const WithDetail: Story = { args: { name: 'Custom Data Type', propertyEditorUiAlias: 'Umb.DataType.CustomUI', - propertyEditorModelAlias: 'UmbracoInput.JSON', + propertyEditorSchemaAlias: 'UmbracoInput.JSON', detail: 'With some custom details', }, }; @@ -32,14 +32,14 @@ export const WithSlots: Story = { args: { name: 'Custom Data Type', propertyEditorUiAlias: 'Umb.DataTypeInput.CustomUI', - propertyEditorModelAlias: 'Umbraco.JSON', + propertyEditorSchemaAlias: 'Umbraco.JSON', detail: 'With some custom details', }, render: (args) => html`
10
diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/data-type-workspace.context.ts index 074f6f8cb6..8c00f1b1f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/data-type-workspace.context.ts @@ -55,7 +55,7 @@ export class UmbDataTypeWorkspaceContext this.#data.update({ name }); } - setPropertyEditorAlias(alias?: string) { + setPropertyEditorSchemaAlias(alias?: string) { this.#data.update({ propertyEditorAlias: alias }); } setPropertyEditorUiAlias(alias?: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/views/details/data-type-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/views/details/data-type-details-workspace-view.element.ts index 9ecc20c214..d623e06fb9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/views/details/data-type-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/settings/data-types/workspace/views/details/data-type-details-workspace-view.element.ts @@ -32,7 +32,7 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement private _propertyEditorUiAlias?: string; @state() - private _propertyEditorAlias?: string; + private _propertyEditorSchemaAlias?: string; @state() private _data: Array = []; @@ -69,13 +69,13 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement if (this._dataType.propertyEditorAlias) { // Get the property editor UI alias from the property editor alias: this.observe( - umbExtensionsRegistry.getByTypeAndAlias('propertyEditorModel', this._dataType.propertyEditorAlias), - (propertyEditorModel) => { - // TODO: show error. We have stored a PropertyEditorModelAlias and can't find the PropertyEditorModel in the registry. - if (!propertyEditorModel) return; - this._setPropertyEditorUiAlias(propertyEditorModel.meta.defaultPropertyEditorUiAlias ?? undefined); + umbExtensionsRegistry.getByTypeAndAlias('propertyEditorSchema', this._dataType.propertyEditorAlias), + (propertyEditorSchema) => { + // TODO: show error. We have stored a propertyEditorSchemaAlias and can't find the PropertyEditorSchema in the registry. + if (!propertyEditorSchema) return; + this._setPropertyEditorUiAlias(propertyEditorSchema.meta.defaultPropertyEditorUiAlias ?? undefined); }, - '_observePropertyEditorModelForDefaultUI' + '_observepropertyEditorSchemaForDefaultUI' ); } else { this._setPropertyEditorUiAlias(undefined); @@ -105,8 +105,8 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement return; } - // remove the '_observePropertyEditorModelForDefaultUI' controller, as we do not want to observe for default value anymore: - this.removeControllerByUnique('_observePropertyEditorModelForDefaultUI'); + // remove the '_observepropertyEditorSchemaForDefaultUI' controller, as we do not want to observe for default value anymore: + this.removeControllerByUnique('_observepropertyEditorSchemaForDefaultUI'); this.observe( umbExtensionsRegistry.getByTypeAndAlias('propertyEditorUi', propertyEditorUiAlias), @@ -117,9 +117,9 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement this._propertyEditorUiName = propertyEditorUI?.meta.label ?? propertyEditorUI?.name ?? ''; this._propertyEditorUiAlias = propertyEditorUI?.alias ?? ''; this._propertyEditorUiIcon = propertyEditorUI?.meta.icon ?? ''; - this._propertyEditorAlias = propertyEditorUI?.meta.propertyEditorAlias ?? ''; + this._propertyEditorSchemaAlias = propertyEditorUI?.meta.propertyEditorSchemaAlias ?? ''; - this._workspaceContext?.setPropertyEditorAlias(this._propertyEditorAlias); + this._workspaceContext?.setPropertyEditorSchemaAlias(this._propertyEditorSchemaAlias); }, '_observePropertyEditorUI' ); @@ -158,7 +158,7 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement slot="editor" name=${this._propertyEditorUiName} alias=${this._propertyEditorUiAlias} - property-editor-model-alias=${this._propertyEditorAlias} + property-editor-model-alias=${this._propertyEditorSchemaAlias} border> @@ -180,7 +180,7 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement private _renderConfig() { return html` - ${this._propertyEditorAlias && this._propertyEditorUiAlias + ${this._propertyEditorSchemaAlias && this._propertyEditorUiAlias ? html` void; - #initialized = false; + #init; constructor(host: UmbControllerHostElement) { this.#host = host; this.#searchDataSource = new UmbLogSearchesServerDataSource(this.#host); this.#messagesDataSource = new UmbLogMessagesServerDataSource(this.#host); - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { + this.#init = new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { this.#notificationService = instance; - this.#checkIfInitialized(); - }); - } - - #init() { - // TODO: This would only works with one user of this method. If two, the first one would be forgotten, but maybe its alright for now as I guess this is temporary. - return new Promise((resolve) => { - this.#initialized ? resolve() : (this.#initResolver = resolve); - }); - } - - #checkIfInitialized() { - if (this.#notificationService) { - this.#initialized = true; - this.#initResolver?.(); - } + }).asPromise(); } async getSavedSearches({ skip, take }: { skip: number; take: number }) { - await this.#init(); + await this.#init; return this.#searchDataSource.getAllSavedSearches({ skip, take }); } async saveSearch({ name, query }: SavedLogSearchPresenationBaseModel) { - await this.#init(); + await this.#init; this.#searchDataSource.postLogViewerSavedSearch({ name, query }); } async removeSearch({ name }: { name: string }) { - await this.#init(); + await this.#init; this.#searchDataSource.deleteSavedSearchByName({ name }); } @@ -68,13 +52,13 @@ export class UmbLogViewerRepository { startDate?: string; endDate?: string; }) { - await this.#init(); + await this.#init; return this.#messagesDataSource.getLogViewerMessageTemplate({ skip, take, startDate, endDate }); } async getLogCount({ startDate, endDate }: { startDate?: string; endDate?: string }) { - await this.#init(); + await this.#init; return this.#messagesDataSource.getLogViewerLevelCount({ startDate, endDate }); } @@ -96,7 +80,7 @@ export class UmbLogViewerRepository { startDate?: string; endDate?: string; }) { - await this.#init(); + await this.#init; return this.#messagesDataSource.getLogViewerLogs({ skip, @@ -110,12 +94,12 @@ export class UmbLogViewerRepository { } async getLogLevels({ skip = 0, take = 100 }: { skip: number; take: number }) { - await this.#init(); + await this.#init; return this.#messagesDataSource.getLogViewerLevel({ skip, take }); } async getLogViewerValidateLogsSize({ startDate, endDate }: { startDate?: string; endDate?: string }) { - await this.#init(); + await this.#init; return this.#messagesDataSource.getLogViewerValidateLogsSize({ startDate, endDate }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/config/storage-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/config/storage-type/manifests.ts index e695647031..3f643de127 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/config/storage-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/config/storage-type/manifests.ts @@ -7,7 +7,7 @@ export const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-tags-storage-type.element.js'), meta: { label: 'Tags Storage Type', - propertyEditorAlias: '', + propertyEditorSchemaAlias: '', icon: 'umb:autofill', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/manifests.ts index 6288f41ee2..81443609cf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tags/property-editors/tags/manifests.ts @@ -8,7 +8,7 @@ const manifest: ManifestPropertyEditorUi = { loader: () => import('./property-editor-ui-tags.element.js'), meta: { label: 'Tags', - propertyEditorAlias: 'Umbraco.Tags', + propertyEditorSchemaAlias: 'Umbraco.Tags', icon: 'umb:tags', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts index 8f8bc13c6b..5e1a4e41b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/stylesheet.repository.ts @@ -2,7 +2,6 @@ import { UmbStylesheetTreeStore, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN } from import { UmbStylesheetTreeServerDataSource } from './sources/stylesheet.tree.server.data.js'; import { UmbStylesheetServerDataSource } from './sources/stylesheet.server.data.js'; import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/backoffice/notification'; import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api'; import { UmbTreeRepository } from '@umbraco-cms/backoffice/repository'; import { FileSystemTreeItemPresentationModel } from '@umbraco-cms/backoffice/backend-api'; @@ -15,9 +14,7 @@ export class UmbStylesheetRepository #dataSource; #treeDataSource; #treeStore?: UmbStylesheetTreeStore; - #notificationContext?: UmbNotificationContext; - #initResolver?: () => void; - #initialized = false; + #init; constructor(host: UmbControllerHostElement) { this.#host = host; @@ -26,26 +23,9 @@ export class UmbStylesheetRepository this.#dataSource = new UmbStylesheetServerDataSource(this.#host); this.#treeDataSource = new UmbStylesheetTreeServerDataSource(this.#host); - new UmbContextConsumerController(this.#host, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN, (instance) => { + this.#init = new UmbContextConsumerController(this.#host, UMB_STYLESHEET_TREE_STORE_CONTEXT_TOKEN, (instance) => { this.#treeStore = instance; - this.#checkIfInitialized(); - }); - - new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => { - this.#notificationContext = instance; - this.#checkIfInitialized(); - }); - } - - #init = new Promise((resolve) => { - this.#initialized ? resolve() : (this.#initResolver = resolve); - }); - - #checkIfInitialized() { - if (this.#treeStore && this.#notificationContext) { - this.#initialized = true; - this.#initResolver?.(); - } + }).asPromise(); } // TREE: diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts index fb62e2ec6d..75849c1841 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts @@ -115,40 +115,7 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement { } #resetMasterTemplate() { - this.#setMasterTemplateId(null); - } - - async #setMasterTemplateId(id: string | null) { - //root item selected - if (id === '') return; - - if (this._content === null || this._content === undefined) return; - const layoutBlockRegex = /(@{[\s\S][^if]*?Layout\s*?=\s*?)("[^"]*?"|null)(;[\s\S]*?})/gi; - const masterTemplate = await this.#templateWorkspaceContext?.setMasterTemplate(id); - - //Reset master template or is did not exist and the declaration exists - if (masterTemplate === null && layoutBlockRegex.test(this._content)) { - const string = this._content?.replace(layoutBlockRegex, `$1null$3`); - this.#templateWorkspaceContext?.setContent(string); - return; - } - - //No declaration and no valid id - do nothing - if (masterTemplate === null) return; - - //if has master template in the content - if (layoutBlockRegex.test(this._content)) { - const string = this._content?.replace(layoutBlockRegex, `$1"${masterTemplate?.name}.cshtml"$3`); - this.#templateWorkspaceContext?.setContent(string); - return; - } - - //if no master template in the content insert it at the beginning - const string = `@{ - Layout = "${masterTemplate?.name}.cshtml"; -} -${this._content}`; - this.#templateWorkspaceContext?.setContent(string); + this.#templateWorkspaceContext?.setMasterTemplate(null); } #openMasterTemplatePicker() { @@ -161,7 +128,7 @@ ${this._content}`; modalContext?.onSubmit().then((data) => { if (!data.selection) return; - this.#setMasterTemplateId(data.selection[0] ?? ''); + this.#templateWorkspaceContext?.setMasterTemplate(data.selection[0] ?? ''); }); } @@ -200,7 +167,7 @@ ${this._content}`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index 4773a6af8f..0837506049 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -19,6 +19,7 @@ export class UmbTemplateWorkspaceContext extends UmbWorkspaceContext data?.alias); content = createObservablePart(this.#data, (data) => data?.content); id = createObservablePart(this.#data, (data) => data?.id); + masterTemplateID = createObservablePart(this.#data, (data) => data?.masterTemplateId); #isCodeEditorReady = new UmbBooleanState(false); isCodeEditorReady = this.#isCodeEditorReady.asObservable(); @@ -61,47 +62,68 @@ export class UmbTemplateWorkspaceContext extends UmbWorkspaceContext { + const currentContent = this.#data.getValue()?.content; + const newMasterTemplateAlias = this.#masterTemplate?.getValue()?.alias; + const hasLayoutBlock = this.getHasLayoutBlock(); + + if (this.#masterTemplate.getValue() === null && hasLayoutBlock && currentContent) { + const newString = currentContent.replace(this.getLayoutBlockRegexPattern(), `$1null$3`); + this.setContent(newString); + return; + } + + //if has layout block in the content + if (hasLayoutBlock && currentContent) { + const string = currentContent.replace( + this.getLayoutBlockRegexPattern(), + `$1"${newMasterTemplateAlias}.cshtml"$3` + ); + this.setContent(string); + return; + } + + //if no layout block in the content insert it at the beginning + const string = `@{ + Layout = "${newMasterTemplateAlias}.cshtml"; +} +${currentContent}`; + this.setContent(string); + }; + public async save() { const template = this.#data.getValue(); const isNew = this.getIsNew(); @@ -125,6 +147,8 @@ export class UmbTemplateWorkspaceContext extends UmbWorkspaceContext readdirSync(path).filter((folder) => lstatSync(`${path}/${folder}`).isDirectory()); const createModuleDescriptors = (folderName) => readFolders(`./src/${folderName}`).map((moduleName) => { diff --git a/src/Umbraco.Web.UI.Client/src/shared/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/shared/repository/repository-items.manager.ts index b3b2b9319f..75f08fb040 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/repository/repository-items.manager.ts @@ -9,7 +9,12 @@ export class UmbRepositoryItemsManager; #getUnique: (entry: ItemType) => string | undefined; - init: Promise; + #init: Promise; + + // the init promise is used externally for recognizing when the manager is ready. + public get init() { + return this.#init; + } #uniques = new UmbArrayState([]); uniques = this.#uniques.asObservable(); @@ -29,7 +34,7 @@ export class UmbRepositoryItemsManager entry.id || ''); - this.init = new UmbExtensionClassInitializer(host, 'repository', repositoryAlias, (repository) => { + this.#init = new UmbExtensionClassInitializer(host, 'repository', repositoryAlias, (repository) => { // TODO: Some test that this repository is a items repository? this.repository = repository as UmbItemRepository; }).asPromise(); @@ -51,7 +56,7 @@ export class UmbRepositoryItemsManager this is NOT a full reimplementation of the existing media helper service, currently +// contains only functions referenced by the TinyMCE editor + +import { Editor, EditorEvent } from "tinymce"; + +export class UmbMediaHelper { + + /** + * + * @param editor + * @param imageDomElement + * @param imgUrl + */ + async sizeImageInEditor(editor: Editor, imageDomElement: HTMLElement, imgUrl?: string) { + const size = editor.dom.getSize(imageDomElement); + const maxImageSize = editor.options.get('maxImageSize'); + + if (maxImageSize && maxImageSize > 0) { + const newSize = this.scaleToMaxSize(maxImageSize, size.w, size.h); + + editor.dom.setAttribs(imageDomElement, { width: Math.round(newSize.width), height: Math.round(newSize.height) }); + + // Images inserted via Media Picker will have a URL we can use for ImageResizer QueryStrings + // Images pasted/dragged in are not persisted to media until saved & thus will need to be added + if (imgUrl) { + const resizedImgUrl = await this.getProcessedImageUrl(imgUrl, { + width: newSize.width, + height: newSize.height, + }); + + editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl); + } + + editor.execCommand('mceAutoResize', false); + } + } + + /** + * + * @param maxSize + * @param width + * @param height + * @returns + */ + scaleToMaxSize(maxSize: number, width: number, height: number) { + const retval = { width, height }; + + const maxWidth = maxSize; // Max width for the image + const maxHeight = maxSize; // Max height for the image + let ratio = 0; // Used for aspect ratio + + // Check if the current width is larger than the max + if (width > maxWidth) { + ratio = maxWidth / width; // get ratio for scaling image + + retval.width = maxWidth; + retval.height = height * ratio; + + height = height * ratio; // Reset height to match scaled image + width = width * ratio; // Reset width to match scaled image + } + + // Check if current height is larger than max + if (height > maxHeight) { + ratio = maxHeight / height; // get ratio for scaling image + + retval.height = maxHeight; + retval.width = width * ratio; + width = width * ratio; // Reset width to match scaled image + } + + return retval; + } + + /** + * + * @param imagePath + * @param options + * @returns + */ + async getProcessedImageUrl(imagePath: string, options: any) { + if (!options) { + return imagePath; + } + + // TODO => use backend cli when available + const result = await fetch('/umbraco/management/api/v1/images/GetProcessedImageUrl'); + const url = await result.json() as string; + + return url; + } + + /** + * + * @param editor + */ + async uploadBlobImages(editor: Editor) { + const content = editor.getContent(); + + // Upload BLOB images (dragged/pasted ones) + // find src attribute where value starts with `blob:` + // search is case-insensitive and allows single or double quotes + if (content.search(/src=["']blob:.*?["']/gi) !== -1) { + const data = await editor.uploadImages(); + // Once all images have been uploaded + data.forEach((item) => { + // Skip items that failed upload + if (item.status === false) { + return; + } + + // Select img element + const img = item.element; + + // Get img src + const imgSrc = img.getAttribute('src'); + const tmpLocation = localStorage.get(`tinymce__${imgSrc}`); + + // Select the img & add new attr which we can search for + // When its being persisted in RTE property editor + // To create a media item & delete this tmp one etc + editor.dom.setAttrib(img, 'data-tmpimg', tmpLocation); + + // Resize the image to the max size configured + // NOTE: no imagesrc passed into func as the src is blob://... + // We will append ImageResizing Querystrings on perist to DB with node save + this.sizeImageInEditor(editor, img); + }); + + // Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute + // This is most likely seen as a duplicate image that has already been uploaded + // editor.uploadImages() does not give us any indiciation that the image been uploaded already + const blobImageWithNoTmpImgAttribute = editor.dom.select('img[src^="blob:"]:not([data-tmpimg])'); + + //For each of these selected items + blobImageWithNoTmpImgAttribute.forEach((imageElement) => { + const blobSrcUri = editor.dom.getAttrib(imageElement, 'src'); + + // Find the same image uploaded (Should be in LocalStorage) + // May already exist in the editor as duplicate image + // OR added to the RTE, deleted & re-added again + // So lets fetch the tempurl out of localstorage for that blob URI item + + const tmpLocation = localStorage.get(`tinymce__${blobSrcUri}`); + if (tmpLocation) { + this.sizeImageInEditor(editor, imageElement); + editor.dom.setAttrib(imageElement, 'data-tmpimg', tmpLocation); + } + }); + } + + if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) { + /** prevent injecting arbitrary JavaScript execution in on-attributes. */ + const allNodes = Array.from(editor.dom.doc.getElementsByTagName('*')); + allNodes.forEach((node) => { + for (let i = 0; i < node.attributes.length; i++) { + if (node.attributes[i].name.startsWith('on')) { + node.removeAttribute(node.attributes[i].name); + } + } + }); + } + } + + /** + * + * @param e + * @returns + */ + async onResize( + e: EditorEvent<{ + target: HTMLElement; + width: number; + height: number; + origin: string; + }> + ) { + const srcAttr = e.target.getAttribute('src'); + + if (!srcAttr) { + return; + } + + const path = srcAttr.split('?')[0]; + const resizedPath = await this.getProcessedImageUrl(path, { + width: e.width, + height: e.height, + mode: 'max', + }); + + e.target.setAttribute('data-mce-src', resizedPath); + } +} diff --git a/src/Umbraco.Web.UI.Client/storybook/stories/extending/property-editors.mdx b/src/Umbraco.Web.UI.Client/storybook/stories/extending/property-editors.mdx index a4ce9e22a6..8b0c42db1e 100644 --- a/src/Umbraco.Web.UI.Client/storybook/stories/extending/property-editors.mdx +++ b/src/Umbraco.Web.UI.Client/storybook/stories/extending/property-editors.mdx @@ -26,7 +26,7 @@ import { Meta } from '@storybook/addon-docs'; ```json { - "type": "propertyEditorModel", + "type": "propertyEditorSchema", "name": "Text Box", "alias": "Umbraco.TextBox", }; @@ -43,7 +43,7 @@ import { Meta } from '@storybook/addon-docs'; "js": "./my-text-box.element.js", "meta": { "label": "My Text Box", - "propertyEditorModel": "Umbraco.TextBox", + "propertyEditorSchema": "Umbraco.TextBox", "icon": "umb:autofill", "group": "common" } diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 75457a4e95..5223937336 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -26,6 +26,7 @@ "@umbraco-cms/backoffice/external/lodash": ["src/external/lodash"], "@umbraco-cms/backoffice/external/uui": ["src/external/uui"], "@umbraco-cms/backoffice/external/monaco-editor": ["src/external/monaco-editor"], + "@umbraco-cms/backoffice/external/tinymce": ["src/external/tinymce"], "@umbraco-cms/backoffice/backend-api": ["src/external/backend-api"], "@umbraco-cms/backoffice/class-api": ["src/external/class-api"], @@ -56,6 +57,7 @@ "@umbraco-cms/backoffice/entity-bulk-action": ["src/packages/core/entity-bulk-action"], "@umbraco-cms/backoffice/extension-registry": ["src/packages/core/extension-registry"], "@umbraco-cms/backoffice/id": ["src/packages/core/id"], + "@umbraco-cms/backoffice/macro": ["src/packages/core/macro"], "@umbraco-cms/backoffice/menu": ["src/packages/core/menu"], "@umbraco-cms/backoffice/modal": ["src/packages/core/modal"], "@umbraco-cms/backoffice/notification": ["src/packages/core/notification"], diff --git a/src/Umbraco.Web.UI.Client/vite.config.ts b/src/Umbraco.Web.UI.Client/vite.config.ts index 132b97a7c5..ecbef57bf6 100644 --- a/src/Umbraco.Web.UI.Client/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/vite.config.ts @@ -17,6 +17,14 @@ export const plugins: PluginOption[] = [ src: 'src/assets/*.svg', dest: 'umbraco/backoffice/assets', }, + { + src: 'node_modules/tinymce/**/*', + dest: 'umbraco/backoffice/tinymce', + }, + { + src: 'node_modules/tinymce-i18n/langs6/**/*', + dest: 'umbraco/backoffice/tinymce/langs', + }, ], }), viteTSConfigPaths(), diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index 630c2cb6f8..39253e6197 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -40,6 +40,7 @@ export default { '@umbraco-cms/backoffice/external/uuid': './src/external/uuid/index.ts', '@umbraco-cms/backoffice/external/lodash': './src/external/lodash/index.ts', '@umbraco-cms/backoffice/external/monaco-editor': './src/external/monaco-editor/index.ts', + '@umbraco-cms/backoffice/external/tinymce': './src/external/tinymce/index.ts', '@umbraco-cms/backoffice/backend-api': './src/external/backend-api/index.ts', '@umbraco-cms/backoffice/class-api': './src/libs/class-api/index.ts', @@ -71,6 +72,7 @@ export default { '@umbraco-cms/backoffice/entity-bulk-action': './src/packages/core/entity-bulk-action/index.ts', '@umbraco-cms/backoffice/extension-registry': './src/packages/core/extension-registry/index.ts', '@umbraco-cms/backoffice/id': './src/packages/core/id/index.ts', + '@umbraco-cms/backoffice/macro': './src/packages/core/macro/index.ts', '@umbraco-cms/backoffice/menu': './src/packages/core/menu/index.ts', '@umbraco-cms/backoffice/modal': './src/packages/core/modal/index.ts', '@umbraco-cms/backoffice/notification': './src/packages/core/notification/index.ts',