Merge branch 'release/15.0'
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type UmbTiptapToolbarValue = Array<Array<Array<string>>>;
|
||||
@@ -132,298 +132,6 @@ const coreExtensions: Array<ManifestTiptapExtension> = [
|
||||
];
|
||||
|
||||
const toolbarExtensions: Array<ManifestTiptapToolbarExtension> = [
|
||||
{
|
||||
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<ManifestTiptapToolbarExtension> = [
|
||||
{
|
||||
type: 'tiptapToolbarExtension',
|
||||
kind: 'button',
|
||||
@@ -436,24 +144,289 @@ const umbToolbarExtensions: Array<ManifestTiptapToolbarExtension> = [
|
||||
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<ManifestTiptapToolbarExtension> = [
|
||||
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];
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ManifestTiptapToolbarExtension<
|
||||
MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension,
|
||||
> extends ManifestElementAndApi<UmbControllerHostElement, UmbTiptapToolbarElementApi> {
|
||||
type: 'tiptapToolbarExtension';
|
||||
forExtensions?: Array<string>;
|
||||
meta: MetaType;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Array<Array<string>>>;
|
||||
|
||||
@@ -19,7 +19,7 @@ export const manifests: Array<ManifestTiptapExtension | ManifestTiptapToolbarExt
|
||||
alias: 'Umb.Tiptap.Toolbar.BlockPicker',
|
||||
name: 'Block Picker Tiptap Extension Button',
|
||||
api: () => import('./block-picker-toolbar.extension.js'),
|
||||
weight: 90,
|
||||
forExtensions: ['Umb.Tiptap.Block'],
|
||||
meta: {
|
||||
alias: 'umbblockpicker',
|
||||
icon: 'icon-plugin',
|
||||
|
||||
@@ -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<UmbTiptapExtensionGroupItem>;
|
||||
};
|
||||
|
||||
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<string>([TIPTAP_CORE_EXTENSION_ALIAS]);
|
||||
|
||||
@property({ attribute: false })
|
||||
value?: Array<string> = [];
|
||||
value?: Array<string> = [TIPTAP_CORE_EXTENSION_ALIAS];
|
||||
|
||||
@property({ attribute: false })
|
||||
config?: UmbPropertyEditorConfigCollection;
|
||||
|
||||
@state()
|
||||
private _extensionCategories: UmbTiptapExtensionGroup[] = [];
|
||||
private _extensions: Array<UmbTiptapExtension> = [];
|
||||
|
||||
@state()
|
||||
private _extensionConfigs: UmbTiptapExtensionConfig[] = [];
|
||||
private _groups: Array<UmbTiptapExtensionGroup> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (dataset) => {
|
||||
this.observe(
|
||||
await dataset.propertyValueByAlias<Array<unknown>>('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<unknown>) {
|
||||
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<string>, b: Array<string>) {
|
||||
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<string>) {
|
||||
this.value = value;
|
||||
this.dispatchEvent(new UmbPropertyValueChangeEvent());
|
||||
}
|
||||
|
||||
#syncViewModel() {
|
||||
const items: Array<UmbTiptapExtensionGroupItem> = 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`
|
||||
<div class="extensions">
|
||||
${repeat(
|
||||
this._extensionCategories,
|
||||
(group) => html`
|
||||
<div class="group">
|
||||
<p class="group-name">${this.localize.string(group.group)}</p>
|
||||
${repeat(
|
||||
this._groups,
|
||||
(group) => html`
|
||||
<div class="group">
|
||||
<uui-label>${this.localize.string(group.group)}</uui-label>
|
||||
<ul>
|
||||
${repeat(
|
||||
group.extensions,
|
||||
(item) => html`
|
||||
<div class="extension-item">
|
||||
<uui-button
|
||||
compact
|
||||
class=${item.selected ? 'selected' : ''}
|
||||
<li title=${ifDefined(item.description)}>
|
||||
<uui-checkbox
|
||||
label=${this.localize.string(item.label)}
|
||||
look="outline"
|
||||
.value=${item.alias}
|
||||
@click=${() => this.#onExtensionClick(item)}>
|
||||
<umb-icon name=${item.icon ?? ''}></umb-icon>
|
||||
</uui-button>
|
||||
<uui-button
|
||||
compact
|
||||
label=${this.localize.string(item.label)}
|
||||
look="default"
|
||||
style="--uui-button-content-align:left;"
|
||||
@click=${() => this.#onExtensionClick(item)}></uui-button>
|
||||
</div>
|
||||
value=${item.alias}
|
||||
?checked=${item.selected}
|
||||
?disabled=${this.#disabledExtensions.has(item.alias)}
|
||||
@change=${() => this.#onClick(item)}>
|
||||
<div class="inner">
|
||||
${when(item.icon, () => html`<umb-icon .name=${item.icon}></umb-icon>`)}
|
||||
<span>${this.localize.string(item.label)}</span>
|
||||
</div>
|
||||
</uui-checkbox>
|
||||
</li>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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<string> = new Set();
|
||||
#context = new UmbTiptapToolbarConfigurationContext(this);
|
||||
|
||||
#currentDragItem?: {
|
||||
alias: string;
|
||||
fromPos?: [number, number, number];
|
||||
};
|
||||
#currentDragItem?: { alias: string; fromPos?: [number, number, number] };
|
||||
|
||||
#lookup?: Map<string, UmbTiptapToolbarExtension>;
|
||||
#debouncedFilter = debounce((query: string) => {
|
||||
this._availableExtensions = this.#context.filterExtensions(query);
|
||||
}, 250);
|
||||
|
||||
@state()
|
||||
private _extensions: Array<UmbTiptapToolbarExtension> = [];
|
||||
private _availableExtensions: Array<UmbTiptapToolbarExtension> = [];
|
||||
|
||||
@state()
|
||||
private _toolbar: Array<UmbTiptapToolbarRowViewModel> = [];
|
||||
|
||||
@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<unknown>) {
|
||||
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))}
|
||||
<uui-button look="secondary" @click=${() => this.#addRow(this.#value.length)}>
|
||||
<uui-icon name="add"></uui-icon>
|
||||
<span>Add row</span>
|
||||
</uui-button>
|
||||
${this.#renderExtensions()}
|
||||
`;
|
||||
return html`${this.#renderAvailableItems()} ${this.#renderDesigner()}`;
|
||||
}
|
||||
|
||||
#renderRow(row: string[][], rowIndex: number) {
|
||||
#renderAvailableItems() {
|
||||
return html`
|
||||
<div class="row">
|
||||
${repeat(row, (group, groupIndex) => this.#renderGroup(group, rowIndex, groupIndex))}
|
||||
<uui-button look="secondary" @click=${() => this.#addGroup(rowIndex, row.length)}>
|
||||
<uui-icon name="add"></uui-icon>
|
||||
<span>Add group</span>
|
||||
</uui-button>
|
||||
<uui-button
|
||||
compact
|
||||
color="danger"
|
||||
look="primary"
|
||||
class="remove-row-button ${rowIndex === 0 && row.length === 1 && row[0].length === 0 ? 'hidden' : undefined}"
|
||||
@click=${() => this.#removeRow(rowIndex)}>
|
||||
<umb-icon name="icon-trash"></umb-icon>
|
||||
</uui-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderGroup(group: string[], rowIndex: number, groupIndex: number) {
|
||||
return html`
|
||||
<div
|
||||
class="group"
|
||||
dropzone="move"
|
||||
@dragover=${this.#onDragOver}
|
||||
@drop=${(e: DragEvent) => this.#onDrop(e, [rowIndex, groupIndex, group.length])}>
|
||||
${group.map((alias, itemIndex) => this.#renderItem(alias, rowIndex, groupIndex, itemIndex))}
|
||||
<uui-button
|
||||
compact
|
||||
color="danger"
|
||||
look="primary"
|
||||
class="remove-group-button ${groupIndex === 0 && group.length === 0 ? 'hidden' : undefined}"
|
||||
@click=${() => this.#removeGroup(rowIndex, groupIndex)}>
|
||||
<umb-icon name="icon-trash"></umb-icon>
|
||||
</uui-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(alias: string, rowIndex: number, groupIndex: number, itemIndex: number) {
|
||||
const extension = this.#lookup?.get(alias);
|
||||
if (!extension) return nothing;
|
||||
return html`
|
||||
<div
|
||||
title=${this.localize.string(extension.label)}
|
||||
class="item"
|
||||
draggable="true"
|
||||
@dragend=${this.#onDragEnd}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
|
||||
<umb-icon name=${extension.icon ?? ''}></umb-icon>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderExtensions() {
|
||||
return html`
|
||||
<div class="extensions" dropzone="move" @drop=${this.#onDrop} @dragover=${this.#onDragOver}>
|
||||
${repeat(
|
||||
this._extensions.filter((ext) => !this.#inUse.has(ext.alias)),
|
||||
(extension) => html`
|
||||
<div
|
||||
class="item"
|
||||
draggable="true"
|
||||
title=${this.localize.string(extension.label)}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, extension.alias)}
|
||||
@dragend=${this.#onDragEnd}>
|
||||
<umb-icon name=${extension.icon ?? ''}></umb-icon>
|
||||
<uui-box class="minimal" headline=${this.localize.term('tiptap_toobar_availableItems')}>
|
||||
<div slot="header-actions">
|
||||
<uui-input
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
placeholder=${this.localize.term('placeholders_filter')}
|
||||
@input=${this.#onFilterInput}>
|
||||
<div slot="prepend">
|
||||
<uui-icon name="search"></uui-icon>
|
||||
</div>
|
||||
</uui-input>
|
||||
</div>
|
||||
<div class="available-items" dropzone="move" @drop=${this.#onDrop} @dragover=${this.#onDragOver}>
|
||||
${when(
|
||||
this._availableExtensions.length === 0,
|
||||
() =>
|
||||
html`<umb-localize key="tiptap_toobar_availableItemsEmpty"
|
||||
>There are no toolbar extensions to show</umb-localize
|
||||
>`,
|
||||
() => repeat(this._availableExtensions, (item) => this.#renderAvailableItem(item)),
|
||||
)}
|
||||
</div>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderAvailableItem(item: UmbTiptapToolbarExtension) {
|
||||
const forbidden = !this.#context.isExtensionEnabled(item.alias);
|
||||
const inUse = this.#context.isExtensionInUse(item.alias);
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
draggable="true"
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
?disabled=${forbidden || inUse}
|
||||
@click=${() => this.#onClick(item)}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)}
|
||||
@dragend=${this.#onDragEnd}>
|
||||
<div class="inner">
|
||||
${when(item.icon, () => html`<umb-icon .name=${item.icon}></umb-icon>`)}
|
||||
<span>${this.localize.string(item.label)}</span>
|
||||
</div>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderDesigner() {
|
||||
return html`
|
||||
<uui-box class="minimal" headline=${this.localize.term('tiptap_toolbar_designer')}>
|
||||
<div id="rows">
|
||||
${repeat(
|
||||
this._toolbar,
|
||||
(row) => row.unique,
|
||||
(row, idx) => this.#renderRow(row, idx),
|
||||
)}
|
||||
</div>
|
||||
<uui-button
|
||||
id="btnAddRow"
|
||||
look="placeholder"
|
||||
label=${this.localize.term('tiptap_toolbar_addRow')}
|
||||
@click=${() => this.#context.insertToolbarRow(this._toolbar.length)}></uui-button>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderRow(row?: UmbTiptapToolbarRowViewModel, rowIndex = 0) {
|
||||
if (!row) return nothing;
|
||||
const hideActionBar = this._toolbar.length === 1;
|
||||
return html`
|
||||
<uui-button-inline-create
|
||||
label=${this.localize.term('tiptap_toolbar_addRow')}
|
||||
@click=${() => this.#context?.insertToolbarRow(rowIndex)}></uui-button-inline-create>
|
||||
<div class="row">
|
||||
<div class="groups">
|
||||
<uui-button-inline-create
|
||||
vertical
|
||||
label=${this.localize.term('tiptap_toolbar_addGroup')}
|
||||
@click=${() => this.#context?.insertToolbarGroup(rowIndex, 0)}></uui-button-inline-create>
|
||||
${repeat(
|
||||
row.data,
|
||||
(group) => group.unique,
|
||||
(group, idx) => this.#renderGroup(group, rowIndex, idx),
|
||||
)}
|
||||
</div>
|
||||
${when(
|
||||
!hideActionBar,
|
||||
() => html`
|
||||
<uui-action-bar>
|
||||
<uui-button
|
||||
color="danger"
|
||||
look="secondary"
|
||||
label=${this.localize.term('tiptap_toolbar_removeRow')}
|
||||
@click=${() => this.#context?.removeToolbarRow(rowIndex)}>
|
||||
<uui-icon name="icon-trash"></uui-icon>
|
||||
</uui-button>
|
||||
</uui-action-bar>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#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`
|
||||
<div
|
||||
class="group"
|
||||
dropzone="move"
|
||||
@dragover=${this.#onDragOver}
|
||||
@drop=${(e: DragEvent) => this.#onDrop(e, [rowIndex, groupIndex, group.data.length - 1])}>
|
||||
<div class="items">
|
||||
${when(
|
||||
group?.data.length === 0,
|
||||
() => html`<em><umb-localize key="tiptap_toolbar_addItems">Add items</umb-localize></em>`,
|
||||
() => html`${group!.data.map((alias, idx) => this.#renderItem(alias, rowIndex, groupIndex, idx))}`,
|
||||
)}
|
||||
</div>
|
||||
${when(
|
||||
!hideActionBar,
|
||||
() => html`
|
||||
<uui-action-bar>
|
||||
<uui-button
|
||||
color="danger"
|
||||
look="secondary"
|
||||
label=${this.localize.term('tiptap_toolbar_removeGroup')}
|
||||
@click=${() => this.#context?.removeToolbarGroup(rowIndex, groupIndex)}>
|
||||
<uui-icon name="icon-trash"></uui-icon>
|
||||
</uui-button>
|
||||
</uui-action-bar>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<uui-button-inline-create
|
||||
vertical
|
||||
label=${this.localize.term('tiptap_toolbar_addGroup')}
|
||||
@click=${() => this.#context?.insertToolbarGroup(rowIndex, groupIndex + 1)}></uui-button-inline-create>
|
||||
`;
|
||||
}
|
||||
|
||||
#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`
|
||||
<uui-button
|
||||
compact
|
||||
class=${forbidden ? 'forbidden' : ''}
|
||||
draggable="true"
|
||||
look=${forbidden ? 'placeholder' : 'outline'}
|
||||
title=${this.localize.string(item.label)}
|
||||
?disabled=${forbidden}
|
||||
@click=${() => this.#context.removeToolbarItem([rowIndex, groupIndex, itemIndex])}
|
||||
@dragend=${this.#onDragEnd}
|
||||
@dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}>
|
||||
<div class="inner">
|
||||
${when(
|
||||
item.icon,
|
||||
() => html`<umb-icon .name=${item.icon}></umb-icon>`,
|
||||
() => html`<span>${this.localize.string(item.label)}</span>`,
|
||||
)}
|
||||
</div>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -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<UmbTiptapToolbarConfigurationContext> {
|
||||
#extensions = new UmbArrayState<UmbTiptapToolbarExtension>([], (x) => x.alias);
|
||||
public readonly extensions = this.#extensions.asObservable();
|
||||
|
||||
#reload = new UmbBooleanState(false);
|
||||
public readonly reload = this.#reload.asObservable();
|
||||
|
||||
#extensionsEnabled = new Set<string>();
|
||||
|
||||
#extensionsInUse = new Set<string>();
|
||||
|
||||
#lookup?: Map<string, UmbTiptapToolbarExtension>;
|
||||
|
||||
#toolbar = new UmbArrayState<UmbTiptapToolbarRowViewModel>([], (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<Array<string>>('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<UmbTiptapToolbarExtension> {
|
||||
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<UmbTiptapToolbarGroupViewModel>) {
|
||||
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>(
|
||||
'UmbTiptapToolbarConfigurationContext',
|
||||
);
|
||||
@@ -13,20 +13,24 @@ export const manifests: Array<ManifestPropertyEditorUi> = [
|
||||
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',
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export type UmbTiptapToolbarExtension = {
|
||||
alias: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
dependencies?: Array<string>;
|
||||
};
|
||||
|
||||
export type UmbTiptapToolbarSortableViewModel<T> = { unique: string; data: T };
|
||||
export type UmbTiptapToolbarRowViewModel = UmbTiptapToolbarSortableViewModel<Array<UmbTiptapToolbarGroupViewModel>>;
|
||||
export type UmbTiptapToolbarGroupViewModel = UmbTiptapToolbarSortableViewModel<Array<string>>;
|
||||
Reference in New Issue
Block a user