diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 968a0fe484..a21a13ac1e 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2650,5 +2650,15 @@ export default { extGroup_interactive: 'Interactive elements', extGroup_media: 'Embeds and media', extGroup_structure: 'Content structure', + extGroup_unknown: 'Uncategorized', + toobar_availableItems: 'Available toolbar items', + toobar_availableItemsEmpty: 'There are no toolbar extensions to show', + toolbar_designer: 'Toolbar designer', + toolbar_addRow: 'Add row configuration', + toolbar_addGroup: 'Add group', + toolbar_addItems: 'Add items', + toolbar_removeRow: 'Remove row', + toolbar_removeGroup: 'Remove group', + toolbar_removeItem: 'Remove item', }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 4197a5075a..e0f083f65a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -1,4 +1,5 @@ -import type { UmbTiptapExtensionApi, UmbTiptapToolbarValue } from '../../extensions/types.js'; +import type { UmbTiptapExtensionApi } from '../../extensions/types.js'; +import type { UmbTiptapToolbarValue } from '../types.js'; import { css, customElement, html, property, state, when } from '@umbraco-cms/backoffice/external/lit'; import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts index 678ec6369a..a799d05907 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts @@ -1,4 +1,4 @@ -import type { UmbTiptapToolbarValue } from '../../extensions/types.js'; +import type { UmbTiptapToolbarValue } from '../types.js'; import { css, customElement, html, map, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts new file mode 100644 index 0000000000..5167150c48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts @@ -0,0 +1 @@ +export type UmbTiptapToolbarValue = Array>>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index ed8f9f7f56..8f27df5139 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -132,298 +132,6 @@ const coreExtensions: Array = [ ]; const toolbarExtensions: Array = [ - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Blockquote', - name: 'Blockquote Tiptap Extension', - api: () => import('./toolbar/blockquote.extension.js'), - weight: 995, - meta: { - alias: 'blockquote', - icon: 'icon-blockquote', - label: 'Blockquote', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Bold', - name: 'Bold Tiptap Extension', - api: () => import('./toolbar/bold.extension.js'), - weight: 999, - meta: { - alias: 'bold', - icon: 'icon-bold', - label: 'Bold', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.CodeBlock', - name: 'Code Block Tiptap Extension', - api: () => import('./toolbar/code-block.extension.js'), - weight: 994, - meta: { - alias: 'codeBlock', - icon: 'icon-code', - label: 'Code Block', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.BulletList', - name: 'Bullet List Tiptap Extension', - api: () => import('./toolbar/bullet-list.extension.js'), - weight: 993, - meta: { - alias: 'bulletList', - icon: 'icon-bulleted-list', - label: 'Bullet List', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.OrderedList', - name: 'Ordered List Tiptap Extension', - api: () => import('./toolbar/ordered-list.extension.js'), - weight: 992, - meta: { - alias: 'orderedList', - icon: 'icon-ordered-list', - label: 'Ordered List', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Redo', - name: 'Redo Tiptap Extension', - api: () => import('./toolbar/redo.extension.js'), - element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), - weight: 994, - meta: { - alias: 'redo', - icon: 'icon-redo', - label: 'Redo', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Strike', - name: 'Strike Tiptap Extension', - api: () => import('./toolbar/strike.extension.js'), - weight: 996, - meta: { - alias: 'strike', - icon: 'icon-strikethrough', - label: 'Strike', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Subscript', - name: 'Subscript Tiptap Extension', - api: () => import('./toolbar/subscript.extension.js'), - weight: 1010, - meta: { - alias: 'subscript', - icon: 'icon-subscript', - label: 'Subscript', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Superscript', - name: 'Superscript Tiptap Extension', - api: () => import('./toolbar/superscript.extension.js'), - weight: 1011, - meta: { - alias: 'superscript', - icon: 'icon-superscript', - label: 'Superscript', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Table', - name: 'Table Tiptap Extension', - api: () => import('./toolbar/table.extension.js'), - weight: 909, - meta: { - alias: 'table', - icon: 'icon-table', - label: 'Table', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Heading1', - name: 'Heading 1 Tiptap Extension', - api: () => import('./toolbar/heading1.extension.js'), - weight: 949, - meta: { - alias: 'heading1', - icon: 'icon-heading-1', - label: 'Heading 1', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Heading2', - name: 'Heading 2 Tiptap Extension', - api: () => import('./toolbar/heading2.extension.js'), - weight: 948, - meta: { - alias: 'heading2', - icon: 'icon-heading-2', - label: 'Heading 2', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Heading3', - name: 'Heading 3 Tiptap Extension', - api: () => import('./toolbar/heading3.extension.js'), - weight: 947, - meta: { - alias: 'heading3', - icon: 'icon-heading-3', - label: 'Heading 3', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.HorizontalRule', - name: 'Horizontal Rule Tiptap Extension', - api: () => import('./toolbar/horizontal-rule.extension.js'), - weight: 991, - meta: { - alias: 'horizontalRule', - icon: 'icon-horizontal-rule', - label: 'Horizontal Rule', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Italic', - name: 'Italic Tiptap Extension', - api: () => import('./toolbar/italic.extension.js'), - weight: 998, - meta: { - alias: 'italic', - icon: 'icon-italic', - label: 'Italic', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.TextAlignCenter', - name: 'Text Align Center Tiptap Extension', - api: () => import('./toolbar/text-align-center.extension.js'), - weight: 918, - meta: { - alias: 'text-align-center', - icon: 'icon-text-align-center', - label: 'Text Align Center', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.TextAlignJustify', - name: 'Text Align Justify Tiptap Extension', - api: () => import('./toolbar/text-align-justify.extension.js'), - weight: 916, - meta: { - alias: 'text-align-justify', - icon: 'icon-text-align-justify', - label: 'Text Align Justify', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.TextAlignLeft', - name: 'Text Align Left Tiptap Extension', - api: () => import('./toolbar/text-align-left.extension.js'), - weight: 919, - meta: { - alias: 'text-align-left', - icon: 'icon-text-align-left', - label: 'Text Align Left', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.TextAlignRight', - name: 'Text Align Right Tiptap Extension', - api: () => import('./toolbar/text-align-right.extension.js'), - weight: 917, - meta: { - alias: 'text-align-right', - icon: 'icon-text-align-right', - label: 'Text Align Right', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Underline', - name: 'Underline Tiptap Extension', - api: () => import('./toolbar/underline.extension.js'), - weight: 997, - meta: { - alias: 'underline', - icon: 'icon-underline', - label: 'Underline', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Undo', - name: 'Undo Tiptap Extension', - api: () => import('./toolbar/undo.extension.js'), - element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), - weight: 994, - meta: { - alias: 'undo', - icon: 'icon-undo', - label: 'Undo', - }, - }, - { - type: 'tiptapToolbarExtension', - kind: 'button', - alias: 'Umb.Tiptap.Toolbar.Unlink', - name: 'Unlink Tiptap Extension', - api: () => import('./toolbar/unlink.extension.js'), - element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), - weight: 101, - meta: { - alias: 'unlink', - icon: 'icon-unlink', - label: 'Unlink', - }, - }, -]; - -const umbToolbarExtensions: Array = [ { type: 'tiptapToolbarExtension', kind: 'button', @@ -436,24 +144,289 @@ const umbToolbarExtensions: Array = [ label: '#general_viewSourceCode', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Bold', + name: 'Bold Tiptap Extension', + api: () => import('./toolbar/bold.extension.js'), + meta: { + alias: 'bold', + icon: 'icon-bold', + label: 'Bold', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Italic', + name: 'Italic Tiptap Extension', + api: () => import('./toolbar/italic.extension.js'), + meta: { + alias: 'italic', + icon: 'icon-italic', + label: 'Italic', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Underline', + name: 'Underline Tiptap Extension', + api: () => import('./toolbar/underline.extension.js'), + forExtensions: ['Umb.Tiptap.Underline'], + meta: { + alias: 'underline', + icon: 'icon-underline', + label: 'Underline', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Strike', + name: 'Strike Tiptap Extension', + api: () => import('./toolbar/strike.extension.js'), + meta: { + alias: 'strike', + icon: 'icon-strikethrough', + label: 'Strike', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignLeft', + name: 'Text Align Left Tiptap Extension', + api: () => import('./toolbar/text-align-left.extension.js'), + forExtensions: ['Umb.Tiptap.TextAlign'], + meta: { + alias: 'text-align-left', + icon: 'icon-text-align-left', + label: 'Text Align Left', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignCenter', + name: 'Text Align Center Tiptap Extension', + api: () => import('./toolbar/text-align-center.extension.js'), + forExtensions: ['Umb.Tiptap.TextAlign'], + meta: { + alias: 'text-align-center', + icon: 'icon-text-align-center', + label: 'Text Align Center', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignRight', + name: 'Text Align Right Tiptap Extension', + api: () => import('./toolbar/text-align-right.extension.js'), + forExtensions: ['Umb.Tiptap.TextAlign'], + meta: { + alias: 'text-align-right', + icon: 'icon-text-align-right', + label: 'Text Align Right', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignJustify', + name: 'Text Align Justify Tiptap Extension', + api: () => import('./toolbar/text-align-justify.extension.js'), + forExtensions: ['Umb.Tiptap.TextAlign'], + meta: { + alias: 'text-align-justify', + icon: 'icon-text-align-justify', + label: 'Text Align Justify', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Heading1', + name: 'Heading 1 Tiptap Extension', + api: () => import('./toolbar/heading1.extension.js'), + meta: { + alias: 'heading1', + icon: 'icon-heading-1', + label: 'Heading 1', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Heading2', + name: 'Heading 2 Tiptap Extension', + api: () => import('./toolbar/heading2.extension.js'), + meta: { + alias: 'heading2', + icon: 'icon-heading-2', + label: 'Heading 2', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Heading3', + name: 'Heading 3 Tiptap Extension', + api: () => import('./toolbar/heading3.extension.js'), + meta: { + alias: 'heading3', + icon: 'icon-heading-3', + label: 'Heading 3', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.BulletList', + name: 'Bullet List Tiptap Extension', + api: () => import('./toolbar/bullet-list.extension.js'), + meta: { + alias: 'bulletList', + icon: 'icon-bulleted-list', + label: 'Bullet List', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.OrderedList', + name: 'Ordered List Tiptap Extension', + api: () => import('./toolbar/ordered-list.extension.js'), + meta: { + alias: 'orderedList', + icon: 'icon-ordered-list', + label: 'Ordered List', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Blockquote', + name: 'Blockquote Tiptap Extension', + api: () => import('./toolbar/blockquote.extension.js'), + meta: { + alias: 'blockquote', + icon: 'icon-blockquote', + label: 'Blockquote', + }, + }, { type: 'tiptapToolbarExtension', kind: 'button', alias: 'Umb.Tiptap.Toolbar.Link', name: 'Link Tiptap Extension', api: () => import('./toolbar/link.extension.js'), + forExtensions: ['Umb.Tiptap.Link'], meta: { alias: 'umbLink', icon: 'icon-link', label: '#defaultdialogs_urlLinkPicker', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Unlink', + name: 'Unlink Tiptap Extension', + api: () => import('./toolbar/unlink.extension.js'), + element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), + forExtensions: ['Umb.Tiptap.Link'], + meta: { + alias: 'unlink', + icon: 'icon-unlink', + label: 'Unlink', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.CodeBlock', + name: 'Code Block Tiptap Extension', + api: () => import('./toolbar/code-block.extension.js'), + meta: { + alias: 'codeBlock', + icon: 'icon-code', + label: 'Code Block', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Subscript', + name: 'Subscript Tiptap Extension', + api: () => import('./toolbar/subscript.extension.js'), + forExtensions: ['Umb.Tiptap.Subscript'], + meta: { + alias: 'subscript', + icon: 'icon-subscript', + label: 'Subscript', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Superscript', + name: 'Superscript Tiptap Extension', + api: () => import('./toolbar/superscript.extension.js'), + forExtensions: ['Umb.Tiptap.Superscript'], + meta: { + alias: 'superscript', + icon: 'icon-superscript', + label: 'Superscript', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.HorizontalRule', + name: 'Horizontal Rule Tiptap Extension', + api: () => import('./toolbar/horizontal-rule.extension.js'), + meta: { + alias: 'horizontalRule', + icon: 'icon-horizontal-rule', + label: 'Horizontal Rule', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Undo', + name: 'Undo Tiptap Extension', + api: () => import('./toolbar/undo.extension.js'), + element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), + meta: { + alias: 'undo', + icon: 'icon-undo', + label: 'Undo', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Redo', + name: 'Redo Tiptap Extension', + api: () => import('./toolbar/redo.extension.js'), + element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), + meta: { + alias: 'redo', + icon: 'icon-redo', + label: 'Redo', + }, + }, { type: 'tiptapToolbarExtension', kind: 'button', alias: 'Umb.Tiptap.Toolbar.MediaPicker', name: 'Media Picker Tiptap Extension', api: () => import('./toolbar/media-picker.extension.js'), + forExtensions: ['Umb.Tiptap.Figure', 'Umb.Tiptap.Image'], meta: { alias: 'umbMedia', icon: 'icon-picture', @@ -466,14 +439,28 @@ const umbToolbarExtensions: Array = [ alias: 'Umb.Tiptap.Toolbar.EmbeddedMedia', name: 'Embedded Media Tiptap Extension', api: () => import('./toolbar/embedded-media.extension.js'), + forExtensions: ['Umb.Tiptap.Embed'], meta: { alias: 'umbEmbeddedMedia', icon: 'icon-embed', label: '#general_embed', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Table', + name: 'Table Tiptap Extension', + api: () => import('./toolbar/table.extension.js'), + forExtensions: ['Umb.Tiptap.Table'], + meta: { + alias: 'table', + icon: 'icon-table', + label: 'Table', + }, + }, ]; -const extensions = [...coreExtensions, ...toolbarExtensions, ...umbToolbarExtensions]; +const extensions = [...coreExtensions, ...toolbarExtensions]; export const manifests = [...kinds, ...extensions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar-extension.ts index a97d658328..3c178b4e85 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar-extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar-extension.ts @@ -6,6 +6,7 @@ export interface ManifestTiptapToolbarExtension< MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension, > extends ManifestElementAndApi { type: 'tiptapToolbarExtension'; + forExtensions?: Array; meta: MetaType; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts index 927d22cf63..ec2bcc1f8c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts @@ -98,5 +98,3 @@ export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase i return editor && this.manifest?.meta.alias ? editor?.isActive(this.manifest.meta.alias) : false; } } - -export type UmbTiptapToolbarValue = Array>>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/plugins/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/plugins/manifests.ts index aafd570b89..7701a5f10a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/plugins/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/plugins/manifests.ts @@ -19,7 +19,7 @@ export const manifests: Array import('./block-picker-toolbar.extension.js'), - weight: 90, + forExtensions: ['Umb.Tiptap.Block'], meta: { alias: 'umbblockpicker', icon: 'icon-plugin', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts index d020ec400d..3762eae369 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts @@ -1,33 +1,44 @@ -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { customElement, css, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { - UmbPropertyValueChangeEvent, - type UmbPropertyEditorConfigCollection, - type UmbPropertyEditorUiElement, + customElement, + css, + html, + ifDefined, + nothing, + property, + state, + repeat, + when, +} from '@umbraco-cms/backoffice/external/lit'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import type { + UmbPropertyEditorConfigCollection, + UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -type UmbTiptapExtensionConfig = { +type UmbTiptapExtension = { alias: string; label: string; icon?: string; - group: string; + group?: string; + description?: string; }; -type UmbTiptapExtensionGroupItem = { - alias: string; - label: string; - icon?: string; +type UmbTiptapExtensionGroupItem = UmbTiptapExtension & { selected: boolean; }; type UmbTiptapExtensionGroup = { group: string; - extensions: UmbTiptapExtensionGroupItem[]; + extensions: Array; }; +const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials'; +const TIPTAP_BLOCK_EXTENSION_ALIAS = 'Umb.Tiptap.Block'; + const elementName = 'umb-property-editor-ui-tiptap-extensions-configuration'; @customElement(elementName) @@ -35,167 +46,177 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement { + #disabledExtensions = new Set([TIPTAP_CORE_EXTENSION_ALIAS]); + @property({ attribute: false }) - value?: Array = []; + value?: Array = [TIPTAP_CORE_EXTENSION_ALIAS]; @property({ attribute: false }) config?: UmbPropertyEditorConfigCollection; @state() - private _extensionCategories: UmbTiptapExtensionGroup[] = []; + private _extensions: Array = []; @state() - private _extensionConfigs: UmbTiptapExtensionConfig[] = []; + private _groups: Array = []; + + constructor() { + super(); + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (dataset) => { + this.observe( + await dataset.propertyValueByAlias>('blocks'), + (blocks) => { + const tmpValue = this.value ? [...this.value] : []; + + // When blocks are configured, the block extension can be enabled; + // otherwise, the block extension must be disabled. + if (blocks?.length) { + // Check if the block extension is already enabled, if not, add it. + if (!tmpValue.includes(TIPTAP_BLOCK_EXTENSION_ALIAS)) { + tmpValue.push(TIPTAP_BLOCK_EXTENSION_ALIAS); + } + this.#disabledExtensions.delete(TIPTAP_BLOCK_EXTENSION_ALIAS); + } else { + // Check if the block extension is enabled, if so, remove it. + const idx = tmpValue.indexOf(TIPTAP_BLOCK_EXTENSION_ALIAS) ?? -1; + if (idx >= 0) { + tmpValue.splice(idx, 1); + } + this.#disabledExtensions.add(TIPTAP_BLOCK_EXTENSION_ALIAS); + } + + if (!this.value || !this.#isArrayEqualTo(tmpValue, this.value)) { + this.#setValue(tmpValue); + this.#syncViewModel(); + } + }, + '_observeBlocks', + ); + }); + } protected override async firstUpdated(_changedProperties: PropertyValueMap) { super.firstUpdated(_changedProperties); this.observe(umbExtensionsRegistry.byType('tiptapExtension'), (extensions) => { - this._extensionConfigs = extensions + this._extensions = extensions .sort((a, b) => a.alias.localeCompare(b.alias)) - .map((ext) => { - return { - alias: ext.alias, - label: ext.meta.label, - icon: ext.meta.icon, - group: ext.meta.group, - }; - }); + .map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon, group: ext.meta.group })); + + // Hardcoded core extension + this._extensions.unshift({ + alias: TIPTAP_CORE_EXTENSION_ALIAS, + label: 'Rich Text Essentials', + icon: 'icon-browser-window', + group: '#tiptap_extGroup_formatting', + description: 'This is a core extension, it must be enabled', + }); if (!this.value) { // The default value is all extensions enabled - this.value = this._extensionConfigs.map((ext) => ext.alias); - this.dispatchEvent(new UmbPropertyValueChangeEvent()); + this.#setValue(this._extensions.map((ext) => ext.alias)); } - this.#setupExtensionCategories(); + this.#syncViewModel(); }); } - #setupExtensionCategories() { - const useDefault = !this.value; // The default value is all extensions enabled - const withSelectedProperty = this._extensionConfigs.map((extensionConfig) => { - return { - ...extensionConfig, - selected: useDefault ? true : this.value!.includes(extensionConfig.alias), - }; - }); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const grouped = Object.groupBy( - withSelectedProperty, - (item: UmbTiptapExtensionConfig) => item.group || 'Uncategorized', - ); - - this._extensionCategories = Object.keys(grouped) - .sort((a, b) => a.localeCompare(b)) - .map((key) => ({ - group: key, - extensions: grouped[key], - })); + #isArrayEqualTo(a: Array, b: Array) { + return a.length === b.length && a.every((item) => b.includes(item)) && b.every((item) => a.includes(item)); } - #onExtensionClick(item: UmbTiptapExtensionGroupItem) { + #onClick(item: UmbTiptapExtensionGroupItem) { item.selected = !item.selected; - if (!this.value) { - this.value = []; - } + const tmpValue = item.selected + ? [...(this.value ?? []), item.alias] + : (this.value ?? []).filter((alias) => alias !== item.alias); - if (item.selected) { - this.value = [...this.value, item.alias]; - } else { - this.value = this.value.filter((alias) => alias !== item.alias); - } + this.#setValue(tmpValue); + } - this.requestUpdate('_extensionCategories'); + #setValue(value: Array) { + this.value = value; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } + #syncViewModel() { + const items: Array = this._extensions.map((extension) => ({ + ...extension, + selected: this.value!.includes(extension.alias) || extension.alias === TIPTAP_CORE_EXTENSION_ALIAS, + })); + + const uncategorizedLabel = this.localize.term('tiptap_extGroup_unknown'); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const grouped = Object.groupBy(items, (item: UmbTiptapExtensionGroupItem) => item.group || uncategorizedLabel); + + this._groups = Object.keys(grouped) + .sort((a, b) => a.localeCompare(b)) + .map((key) => ({ group: key, extensions: grouped[key] })); + } + override render() { + if (!this._groups.length) return nothing; return html` -
- ${repeat( - this._extensionCategories, - (group) => html` -
-

${this.localize.string(group.group)}

+ ${repeat( + this._groups, + (group) => html` +
+ ${this.localize.string(group.group)} +
    ${repeat( group.extensions, (item) => html` -
    - + this.#onExtensionClick(item)}> - - - this.#onExtensionClick(item)}> -
    + value=${item.alias} + ?checked=${item.selected} + ?disabled=${this.#disabledExtensions.has(item.alias)} + @change=${() => this.#onClick(item)}> +
    + ${when(item.icon, () => html``)} + ${this.localize.string(item.label)} +
    + + `, )} -
- `, - )} -
+ +
+ `, + )} `; } static override readonly styles = [ - UmbTextStyles, css` - uui-icon { - width: unset; - height: unset; - display: flex; - vertical-align: unset; - } - - uui-button.selected { - --uui-button-border-color: var(--uui-color-selected); - --uui-button-border-width: 2px; - } - - .extensions { + :host { display: flex; flex-wrap: wrap; - gap: 16px; - margin-top: 16px; - } - - .extension-item { - display: grid; - grid-template-columns: 36px 1fr; - grid-template-rows: 1fr; - align-items: center; - gap: 9px; + gap: 1rem; } .group { flex: 1; - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px; - background-color: var(--uui-color-surface-alt); - border: 1px solid var(--uui-color-border); - border-radius: 6px; - } - .group-name { - grid-column: 1 / -1; - display: flex; - font-weight: bold; - margin: 0; + ul { + list-style: none; + padding: 0; + margin: 1rem 0 0; + + .inner { + display: flex; + flex-direction: row; + gap: 0.5rem; + + umb-icon { + font-size: 1.2rem; + } + } + } } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts index 7078d2318a..4fd205cead 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -1,16 +1,16 @@ -import type { UmbTiptapToolbarValue } from '../../../extensions/types.js'; -import { customElement, css, html, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbTiptapToolbarConfigurationContext } from '../contexts/tiptap-toolbar-configuration.context.js'; +import type { + UmbTiptapToolbarExtension, + UmbTiptapToolbarGroupViewModel, + UmbTiptapToolbarRowViewModel, +} from '../types.js'; +import type { UmbTiptapToolbarValue } from '../../../components/types.js'; +import { customElement, css, html, property, repeat, state, when, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { debounce } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbPropertyValueChangeEvent, type UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -type UmbTiptapToolbarExtension = { - alias: string; - label: string; - icon: string; -}; const elementName = 'umb-property-editor-ui-tiptap-toolbar-configuration'; @customElement(elementName) @@ -18,56 +18,59 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement { - readonly #inUse: Set = new Set(); + #context = new UmbTiptapToolbarConfigurationContext(this); - #currentDragItem?: { - alias: string; - fromPos?: [number, number, number]; - }; + #currentDragItem?: { alias: string; fromPos?: [number, number, number] }; - #lookup?: Map; + #debouncedFilter = debounce((query: string) => { + this._availableExtensions = this.#context.filterExtensions(query); + }, 250); @state() - private _extensions: Array = []; + private _availableExtensions: Array = []; + + @state() + private _toolbar: Array = []; @property({ attribute: false }) set value(value: UmbTiptapToolbarValue | undefined) { - if (!this.#isValidTiptapToolbarValue(value)) { - this.#value = [[[]]]; - return; - } - - if (value.length > 0) { - this.#value = value.map((rows) => rows.map((groups) => [...groups])); - value.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#inUse.add(alias)))); - } + if (!value) value = [[[]]]; + if (value === this.#value) return; + this.#context.setToolbar(value); } - get value(): UmbTiptapToolbarValue { - return this.#value; + get value(): UmbTiptapToolbarValue | undefined { + return this.#value?.map((rows) => rows.map((groups) => [...groups])); } - #value: UmbTiptapToolbarValue = [[[]]]; + #value?: UmbTiptapToolbarValue; - protected override async firstUpdated(_changedProperties: PropertyValueMap) { - super.firstUpdated(_changedProperties); + constructor() { + super(); - this.observe(umbExtensionsRegistry.byType('tiptapToolbarExtension'), (extensions) => { - this._extensions = extensions.map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon })); - this.#lookup = new Map(this._extensions.map((ext) => [ext.alias, ext])); + this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { + this.observe(this.#context.extensions, (extensions) => { + this._availableExtensions = extensions; + }); + + this.observe(this.#context.reload, (reload) => { + if (reload) { + this.requestUpdate(); + } + }); + + this.observe(this.#context.toolbar, (toolbar) => { + if (!toolbar.length) return; + this._toolbar = toolbar; + this.#value = toolbar.map((rows) => rows.data.map((groups) => [...groups.data])); + propertyContext.setValue(this.#value); + }); }); } - #isValidTiptapToolbarValue(value: unknown): value is UmbTiptapToolbarValue { - if (!Array.isArray(value)) return false; - for (const row of value) { - if (!Array.isArray(row)) return false; - for (const group of row) { - if (!Array.isArray(group)) return false; - for (const alias of group) { - if (typeof alias !== 'string') return false; - } - } - } - return true; + #onClick(item: UmbTiptapToolbarExtension) { + const lastRow = (this.#value?.length ?? 1) - 1; + const lastGroup = (this.#value?.[lastRow].length ?? 1) - 1; + const lastItem = this.#value?.[lastRow][lastGroup].length ?? 0; + this.#context.insertToolbarItem(item.alias, [lastRow, lastGroup, lastItem]); } #onDragStart(event: DragEvent, alias: string, fromPos?: [number, number, number]) { @@ -86,7 +89,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement const { fromPos } = this.#currentDragItem ?? {}; if (!fromPos) return; - this.#removeItem(fromPos); + this.#context.removeToolbarItem(fromPos); } } @@ -96,240 +99,353 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement // Remove item if no destination position is provided if (fromPos && !toPos) { - this.#removeItem(fromPos); + this.#context.removeToolbarItem(fromPos); return; } + // Move item if both source and destination positions are available if (fromPos && toPos) { - this.#moveItem(fromPos, toPos); + this.#context.moveToolbarItem(fromPos, toPos); return; } + // Insert item if an alias and a destination position are provided if (alias && toPos) { - this.#insertItem(alias, toPos); + this.#context.insertToolbarItem(alias, toPos); } } - #moveItem(from: [number, number, number], to: [number, number, number]) { - const [rowIndex, groupIndex, itemIndex] = from; - - // Get the item to move from the 'from' position - const itemToMove = this.#value[rowIndex][groupIndex][itemIndex]; - - // Remove the item from the original position - this.#value[rowIndex][groupIndex].splice(itemIndex, 1); - - this.#insertItem(itemToMove, to); - } - - #insertItem(alias: string, toPos: [number, number, number]) { - const [rowIndex, groupIndex, itemIndex] = toPos; - - // Insert the item into the new position - const inserted = this.#value[rowIndex][groupIndex].splice(itemIndex, 0, alias); - inserted.forEach((alias) => this.#inUse.add(alias)); - - this.dispatchEvent(new UmbPropertyValueChangeEvent()); - } - - #removeItem(from: [number, number, number]) { - const [rowIndex, groupIndex, itemIndex] = from; - - const removed = this.#value[rowIndex][groupIndex].splice(itemIndex, 1); - removed.forEach((alias) => this.#inUse.delete(alias)); - - this.dispatchEvent(new UmbPropertyValueChangeEvent()); - } - - #addGroup(rowIndex: number, groupIndex: number) { - this.#value[rowIndex].splice(groupIndex, 0, []); - this.dispatchEvent(new UmbPropertyValueChangeEvent()); - } - - #removeGroup(rowIndex: number, groupIndex: number) { - if (this.#value[rowIndex].length > groupIndex) { - const removed = this.#value[rowIndex].splice(groupIndex, 1); - removed.forEach((group) => group.forEach((alias) => this.#inUse.delete(alias))); - } - - // Prevent leaving an empty group - if (this.#value[rowIndex].length === 0) { - this.#value[rowIndex][groupIndex] = []; - } - - this.dispatchEvent(new UmbPropertyValueChangeEvent()); - } - - #addRow(rowIndex: number) { - this.#value.splice(rowIndex, 0, [[]]); - this.dispatchEvent(new UmbPropertyValueChangeEvent()); - } - - #removeRow(rowIndex: number) { - if (this.#value.length > rowIndex) { - const removed = this.#value.splice(rowIndex, 1); - removed.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#inUse.delete(alias)))); - } - - // Prevent leaving an empty row - if (this.#value.length === 0) { - this.#value[rowIndex] = [[]]; - } - - this.dispatchEvent(new UmbPropertyValueChangeEvent()); + #onFilterInput(event: InputEvent & { target: HTMLInputElement }) { + const query = (event.target.value ?? '').toLocaleLowerCase(); + this.#debouncedFilter(query); } override render() { - return html` - ${repeat(this.#value, (row, rowIndex) => this.#renderRow(row, rowIndex))} - this.#addRow(this.#value.length)}> - - Add row - - ${this.#renderExtensions()} - `; + return html`${this.#renderAvailableItems()} ${this.#renderDesigner()}`; } - #renderRow(row: string[][], rowIndex: number) { + #renderAvailableItems() { return html` -
- ${repeat(row, (group, groupIndex) => this.#renderGroup(group, rowIndex, groupIndex))} - this.#addGroup(rowIndex, row.length)}> - - Add group - - this.#removeRow(rowIndex)}> - - -
- `; - } - - #renderGroup(group: string[], rowIndex: number, groupIndex: number) { - return html` -
this.#onDrop(e, [rowIndex, groupIndex, group.length])}> - ${group.map((alias, itemIndex) => this.#renderItem(alias, rowIndex, groupIndex, itemIndex))} - this.#removeGroup(rowIndex, groupIndex)}> - - -
- `; - } - - #renderItem(alias: string, rowIndex: number, groupIndex: number, itemIndex: number) { - const extension = this.#lookup?.get(alias); - if (!extension) return nothing; - return html` -
this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}> - -
- `; - } - - #renderExtensions() { - return html` -
- ${repeat( - this._extensions.filter((ext) => !this.#inUse.has(ext.alias)), - (extension) => html` -
this.#onDragStart(e, extension.alias)} - @dragend=${this.#onDragEnd}> - + +
+ +
+
+
+
+
+ ${when( + this._availableExtensions.length === 0, + () => + html`There are no toolbar extensions to show`, + () => repeat(this._availableExtensions, (item) => this.#renderAvailableItem(item)), + )} +
+
+ `; + } + + #renderAvailableItem(item: UmbTiptapToolbarExtension) { + const forbidden = !this.#context.isExtensionEnabled(item.alias); + const inUse = this.#context.isExtensionInUse(item.alias); + return html` + this.#onClick(item)} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)} + @dragend=${this.#onDragEnd}> +
+ ${when(item.icon, () => html``)} + ${this.localize.string(item.label)} +
+
+ `; + } + + #renderDesigner() { + return html` + +
+ ${repeat( + this._toolbar, + (row) => row.unique, + (row, idx) => this.#renderRow(row, idx), + )} +
+ this.#context.insertToolbarRow(this._toolbar.length)}> +
+ `; + } + + #renderRow(row?: UmbTiptapToolbarRowViewModel, rowIndex = 0) { + if (!row) return nothing; + const hideActionBar = this._toolbar.length === 1; + return html` + this.#context?.insertToolbarRow(rowIndex)}> +
+
+ this.#context?.insertToolbarGroup(rowIndex, 0)}> + ${repeat( + row.data, + (group) => group.unique, + (group, idx) => this.#renderGroup(group, rowIndex, idx), + )} +
+ ${when( + !hideActionBar, + () => html` + + this.#context?.removeToolbarRow(rowIndex)}> + + + `, )}
`; } + #renderGroup(group?: UmbTiptapToolbarGroupViewModel, rowIndex = 0, groupIndex = 0) { + if (!group) return nothing; + const hideActionBar = this._toolbar[rowIndex].data.length === 1 && group.data.length === 0; + return html` +
this.#onDrop(e, [rowIndex, groupIndex, group.data.length - 1])}> +
+ ${when( + group?.data.length === 0, + () => html`Add items`, + () => html`${group!.data.map((alias, idx) => this.#renderItem(alias, rowIndex, groupIndex, idx))}`, + )} +
+ ${when( + !hideActionBar, + () => html` + + this.#context?.removeToolbarGroup(rowIndex, groupIndex)}> + + + + `, + )} +
+ this.#context?.insertToolbarGroup(rowIndex, groupIndex + 1)}> + `; + } + + #renderItem(alias: string, rowIndex = 0, groupIndex = 0, itemIndex = 0) { + const item = this.#context?.getExtensionByAlias(alias); + if (!item) return nothing; + const forbidden = !this.#context?.isExtensionEnabled(item.alias); + return html` + this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])} + @dragend=${this.#onDragEnd} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}> +
+ ${when( + item.icon, + () => html``, + () => html`${this.localize.string(item.label)}`, + )} +
+
+ `; + } + static override readonly styles = [ - UmbTextStyles, css` :host { display: flex; flex-direction: column; - gap: 6px; + gap: var(--uui-size-1); } - .extensions { + + uui-box.minimal { + --uui-box-header-padding: 0; + --uui-box-default-padding: var(--uui-size-2) 0; + --uui-box-box-shadow: none; + + [slot='header-actions'] { + margin-bottom: var(--uui-size-2); + + uui-icon { + color: var(--uui-color-border); + } + } + } + + .available-items { display: flex; flex-wrap: wrap; - gap: 3px; - border-radius: var(--uui-border-radius); + gap: var(--uui-size-3); background-color: var(--uui-color-surface-alt); - padding: 6px; - min-height: 30px; - min-width: 30px; - } - .row { - position: relative; - display: flex; - gap: 12px; - } - .group { - position: relative; - display: flex; - gap: 3px; border-radius: var(--uui-border-radius); - background-color: var(--uui-color-surface-alt); - padding: 6px; - min-height: 32px; - min-width: 32px; + padding: var(--uui-size-3); + + uui-button { + --uui-button-font-weight: normal; + + &[draggable='true'], + &[draggable='true'] > .inner { + cursor: move; + } + + &[disabled], + &[disabled] > .inner { + cursor: not-allowed; + } + + &.forbidden { + --color: var(--uui-color-danger); + --color-standalone: var(--uui-color-danger-standalone); + --color-emphasis: var(--uui-color-danger-emphasis); + --color-contrast: var(--uui-color-danger); + --uui-button-contrast-disabled: var(--uui-color-danger); + --uui-button-border-color-disabled: var(--uui-color-danger); + opacity: 0.5; + } + + div { + display: flex; + gap: var(--uui-size-1); + } + } } - .item { - padding: var(--uui-size-space-2); - border: 1px solid var(--uui-color-border); - border-radius: var(--uui-border-radius); - background-color: var(--uui-color-surface); + + #rows { + display: flex; + flex-direction: column; + gap: var(--uui-size-1); + + .row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--uui-size-3); + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + padding: var(--uui-size-3) var(--uui-size-2); + + &:hover { + border-color: var(--uui-button-contrast-hover); + } + + .groups { + flex: 1; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: flex-start; + gap: var(--uui-size-1); + + uui-button-inline-create { + height: 40px; + } + + .group { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: var(--uui-size-3); + + border: 1px dashed var(--uui-color-border-standalone); + border-radius: var(--uui-border-radius); + padding: var(--uui-size-3); + + &:hover { + border-color: var(--uui-button-contrast-hover); + } + + .items { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--uui-size-1); + + uui-button { + --uui-button-font-weight: normal; + + &[draggable='true'], + &[draggable='true'] > .inner { + cursor: move; + } + + &[disabled], + &[disabled] > .inner { + cursor: not-allowed; + } + + &.forbidden { + --color: var(--uui-color-danger); + --color-standalone: var(--uui-color-danger-standalone); + --color-emphasis: var(--uui-color-danger-emphasis); + --color-contrast: var(--uui-color-danger); + --uui-button-contrast-disabled: var(--uui-color-danger); + --uui-button-border-color-disabled: var(--uui-color-danger); + opacity: 0.5; + } + + div { + display: flex; + gap: var(--uui-size-1); + } + } + } + } + } + } + } + + #btnAddRow { + display: block; + margin-top: var(--uui-size-1); + } + + .handle { cursor: move; - display: flex; - box-sizing: border-box; - width: 32px; - height: 32px; - justify-content: center; - } - - .remove-row-button, - .remove-group-button { - display: none; - } - .remove-group-button { - position: absolute; - top: -26px; - left: 50%; - transform: translateX(-50%); - z-index: 1; - } - - .row:hover .remove-row-button:not(.hidden), - .group:hover .remove-group-button:not(.hidden) { - display: flex; - } - umb-icon { - /* Prevents titles from bugging out */ - pointer-events: none; } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts new file mode 100644 index 0000000000..a6d73979c5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -0,0 +1,247 @@ +import type { UmbTiptapToolbarValue } from '../../../components/types.js'; +import type { + UmbTiptapToolbarExtension, + UmbTiptapToolbarGroupViewModel, + UmbTiptapToolbarRowViewModel, +} from '../types.js'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbTiptapToolbarConfigurationContext extends UmbContextBase { + #extensions = new UmbArrayState([], (x) => x.alias); + public readonly extensions = this.#extensions.asObservable(); + + #reload = new UmbBooleanState(false); + public readonly reload = this.#reload.asObservable(); + + #extensionsEnabled = new Set(); + + #extensionsInUse = new Set(); + + #lookup?: Map; + + #toolbar = new UmbArrayState([], (x) => x.unique); + public readonly toolbar = this.#toolbar.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_TIPTAP_TOOLBAR_CONFIGURATION_CONTEXT); + + this.observe(umbExtensionsRegistry.byType('tiptapToolbarExtension'), (extensions) => { + const _extensions = extensions.map((ext) => ({ + alias: ext.alias, + label: ext.meta.label, + icon: ext.meta.icon, + dependencies: ext.forExtensions, + })); + + this.#extensions.setValue(_extensions); + + this.#lookup = new Map(_extensions.map((ext) => [ext.alias, ext])); + }); + + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (dataset) => { + this.observe( + await dataset.propertyValueByAlias>('extensions'), + (extensions) => { + if (extensions) { + this.#extensionsEnabled.clear(); + this.#reload.setValue(false); + + this.#extensions + .getValue() + .filter((x) => !x.dependencies || x.dependencies.every((z) => extensions.includes(z))) + .map((x) => x.alias) + .forEach((alias) => this.#extensionsEnabled.add(alias)); + + this.#reload.setValue(true); + } + }, + '_observeExtensions', + ); + }); + } + + public filterExtensions(query: string): Array { + return this.#extensions + .getValue() + .filter((ext) => ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query)); + } + + public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension | undefined { + return this.#lookup?.get(alias); + } + + public isExtensionEnabled(alias: string): boolean { + return this.#extensionsEnabled.has(alias); + } + + public isExtensionInUse(alias: string): boolean { + return this.#extensionsInUse.has(alias); + } + + public isValidToolbarValue(value: unknown): value is UmbTiptapToolbarValue { + if (!Array.isArray(value)) return false; + for (const row of value) { + if (!Array.isArray(row)) return false; + for (const group of row) { + if (!Array.isArray(group)) return false; + for (const alias of group) { + if (typeof alias !== 'string') return false; + } + } + } + return true; + } + + public insertToolbarItem(alias: string, to: [number, number, number]) { + const toolbar = [...this.#toolbar.getValue()]; + + const [rowIndex, groupIndex, itemIndex] = to; + + const row = toolbar[rowIndex]; + const rowData = [...row.data]; + const group = rowData[groupIndex]; + const items = [...group.data]; + + items.splice(itemIndex, 0, alias); + this.#extensionsInUse.add(alias); + + rowData[groupIndex] = { unique: group.unique, data: items }; + toolbar[rowIndex] = { unique: row.unique, data: rowData }; + + this.#toolbar.setValue(toolbar); + } + + public insertToolbarGroup(rowIndex: number, groupIndex: number) { + const toolbar = [...this.#toolbar.getValue()]; + const row = toolbar[rowIndex]; + const groups = [...row.data]; + groups.splice(groupIndex, 0, { unique: UmbId.new(), data: [] }); + toolbar[rowIndex] = { unique: row.unique, data: groups }; + this.#toolbar.setValue(toolbar); + } + + public insertToolbarRow(rowIndex: number) { + const toolbar = [...this.#toolbar.getValue()]; + toolbar.splice(rowIndex, 0, { unique: UmbId.new(), data: [{ unique: UmbId.new(), data: [] }] }); + this.#toolbar.setValue(toolbar); + } + + public moveToolbarItem(from: [number, number, number], to: [number, number, number]) { + const [fromRowIndex, fromGroupIndex, fromItemIndex] = from; + const [toRowIndex, toGroupIndex, toItemIndex] = to; + + const toolbar = [...this.#toolbar.getValue()]; + + const fromRow = toolbar[fromRowIndex]; + const fromRowData = [...fromRow.data]; + const fromGroup = fromRowData[fromGroupIndex]; + const fromItems = [...fromGroup.data]; + + const toBeMoved = fromItems.splice(fromItemIndex, 1); + + fromRowData[fromGroupIndex] = { unique: fromGroup.unique, data: fromItems }; + toolbar[fromRowIndex] = { unique: fromRow.unique, data: fromRowData }; + + const toRow = toolbar[toRowIndex]; + const toRowData = [...toRow.data]; + const toGroup = toRowData[toGroupIndex]; + const toItems = [...toGroup.data]; + + toItems.splice(toItemIndex, 0, toBeMoved[0]); + + toRowData[toGroupIndex] = { unique: toGroup.unique, data: toItems }; + toolbar[toRowIndex] = { unique: toRow.unique, data: toRowData }; + + this.#toolbar.setValue(toolbar); + } + + public removeToolbarItem(from: [number, number, number]) { + const [rowIndex, groupIndex, itemIndex] = from; + + const toolbar = [...this.#toolbar.getValue()]; + const row = toolbar[rowIndex]; + const rowData = [...row.data]; + const group = rowData[groupIndex]; + const items = [...group.data]; + + const removed = items.splice(itemIndex, 1); + removed.forEach((alias) => this.#extensionsInUse.delete(alias)); + + rowData[groupIndex] = { unique: group.unique, data: items }; + toolbar[rowIndex] = { unique: row.unique, data: rowData }; + + this.#toolbar.setValue(toolbar); + } + + public removeToolbarGroup(rowIndex: number, groupIndex: number) { + const toolbar = [...this.#toolbar.getValue()]; + + if (toolbar[rowIndex].data.length > groupIndex) { + const row = toolbar[rowIndex]; + const groups = [...row.data]; + const removed = groups.splice(groupIndex, 1); + removed.forEach((group) => group.data.forEach((alias) => this.#extensionsInUse.delete(alias))); + toolbar[rowIndex] = { unique: row.unique, data: groups }; + } + + // Prevent leaving an empty group + if (toolbar[rowIndex].data.length === 0) { + toolbar[rowIndex].data[0] = { unique: UmbId.new(), data: [] }; + } + + this.#toolbar.setValue(toolbar); + } + + public removeToolbarRow(rowIndex: number) { + const toolbar = [...this.#toolbar.getValue()]; + + if (toolbar.length > rowIndex) { + const removed = toolbar.splice(rowIndex, 1); + removed.forEach((row) => + row.data.forEach((group) => group.data.forEach((alias) => this.#extensionsInUse.delete(alias))), + ); + } + + // Prevent leaving an empty row + if (toolbar.length === 0) { + toolbar[0] = { unique: UmbId.new(), data: [{ unique: UmbId.new(), data: [] }] }; + } + + this.#toolbar.setValue(toolbar); + } + + public setToolbar(value?: UmbTiptapToolbarValue | null) { + if (!this.isValidToolbarValue(value)) { + value = [[[]]]; + } + + this.#extensionsInUse.clear(); + value.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#extensionsInUse.add(alias)))); + + const toolbar = value.map((row) => ({ + unique: UmbId.new(), + data: row.map((group) => ({ unique: UmbId.new(), data: group })), + })); + + this.#toolbar.setValue(toolbar); + } + + public updateToolbarRow(rowIndex: number, groups: Array) { + const toolbar = [...this.#toolbar.getValue()]; + + const row = toolbar[rowIndex]; + toolbar[rowIndex] = { unique: row.unique, data: groups }; + + this.#toolbar.setValue(toolbar); + } +} + +export const UMB_TIPTAP_TOOLBAR_CONFIGURATION_CONTEXT = new UmbContextToken( + 'UmbTiptapToolbarConfigurationContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts index a2791c4935..b0526ffd57 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts @@ -13,20 +13,24 @@ export const manifests: Array = [ group: 'richContent', settings: { properties: [ - { - alias: 'toolbar', - label: 'Toolbar', - description: 'Pick the toolbar items that should be available when editing', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', - weight: 5, - }, { alias: 'extensions', - label: 'Extensions', - description: 'Extensions to enable', + label: 'Capabilities', + description: `Enable extensions enhance the capabilities of the Tiptap editor. + +_Once enabled, the extensions will be available in the toolbar._`, propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration', weight: 10, }, + { + alias: 'toolbar', + label: 'Toolbar', + description: `Configure the toolbar for the intended editing experience. + +_Drag and drop the available items onto the toolbar designer._`, + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', + weight: 15, + }, { alias: 'dimensions', label: 'Dimensions', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts new file mode 100644 index 0000000000..305cd36967 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts @@ -0,0 +1,10 @@ +export type UmbTiptapToolbarExtension = { + alias: string; + label: string; + icon: string; + dependencies?: Array; +}; + +export type UmbTiptapToolbarSortableViewModel = { unique: string; data: T }; +export type UmbTiptapToolbarRowViewModel = UmbTiptapToolbarSortableViewModel>; +export type UmbTiptapToolbarGroupViewModel = UmbTiptapToolbarSortableViewModel>;