Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { UUIPopoverContainerElement } from '../../uui/index.js';
import { Extension } from '@tiptap/core';
import { Editor } from '@tiptap/core';
import { EditorState, Plugin, PluginKey } from '@tiptap/pm/state';
import { EditorView } from '@tiptap/pm/view';
import type { PluginView } from '@tiptap/pm/state';

export interface UmbTiptapBubbleMenuElement extends HTMLElement {
editor?: Editor;
}

export type UmbBubbleMenuPluginProps = {
unique: string;
placement?: UUIPopoverContainerElement['placement'];
elementName?: string | null;
shouldShow?:
| ((props: { editor: Editor; view: EditorView; state: EditorState; from: number; to: number }) => boolean)
| null;
};

export type UmbBubbleMenuOptions = UmbBubbleMenuPluginProps;

export const UmbBubbleMenu = Extension.create<UmbBubbleMenuOptions>({
name: 'umbBubbleMenu',

addOptions() {
return {
unique: 'umb-tiptap-menu',
placement: 'top',
elementName: null,
shouldShow: null,
};
},

addProseMirrorPlugins() {
if (!this.options.unique || !this.options.elementName) {
return [];
}

return [
UmbBubbleMenuPlugin(this.editor, {
unique: this.options.unique,
placement: this.options.placement,
elementName: this.options.elementName,
shouldShow: this.options.shouldShow,
}),
];
},
});

class UmbBubbleMenuPluginView implements PluginView {
#editor: Editor;

#popover: UUIPopoverContainerElement;

#shouldShow: UmbBubbleMenuPluginProps['shouldShow'];

constructor(editor: Editor, view: EditorView, props: UmbBubbleMenuPluginProps) {
this.#editor = editor;

this.#shouldShow = props.shouldShow ?? null;

this.#popover = document.createElement('uui-popover-container') as UUIPopoverContainerElement;
this.#popover.id = props.unique;
this.#popover.setAttribute('placement', props.placement ?? 'top');
this.#popover.setAttribute('popover', 'manual');

if (props.elementName) {
const menu = document.createElement(props.elementName) as UmbTiptapBubbleMenuElement;
menu.editor = editor;
this.#popover.appendChild(menu);
}

view.dom.parentNode?.appendChild(this.#popover);

this.update(view, null);
}

update(view: EditorView, prevState: EditorState | null) {
const editor = this.#editor;

const { state } = view;
const { selection } = state;

const { ranges } = selection;
const from = Math.min(...ranges.map((range) => range.$from.pos));
const to = Math.max(...ranges.map((range) => range.$to.pos));

const shouldShow = this.#shouldShow?.({ editor, view, state, from, to });

if (!shouldShow) {
this.#popover.hidePopover();
} else {
this.#popover.showPopover();
}
}

destroy() {
this.#popover.remove();
}
}

export const UmbBubbleMenuPlugin = (editor: Editor, props: UmbBubbleMenuPluginProps) => {
return new Plugin({
view(editorView) {
return new UmbBubbleMenuPluginView(editor, editorView, props);
},
});
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { UmbBubbleMenuPlugin } from './tiptap-umb-bubble-menu.extension.js';
import { CellSelection, TableMap } from '@tiptap/pm/tables';
import { Decoration, DecorationSet } from '@tiptap/pm/view';
import { EditorState } from '@tiptap/pm/state';
import { EditorView } from '@tiptap/pm/view';
import { findParentNode, mergeAttributes, Editor, Node } from '@tiptap/core';
import { Decoration, DecorationSet, EditorView } from '@tiptap/pm/view';
import { EditorState, Plugin, Selection, Transaction } from '@tiptap/pm/state';
import { findParentNode, Editor } from '@tiptap/core';
import { Node as PMNode, ResolvedPos } from '@tiptap/pm/model';
import { Plugin } from '@tiptap/pm/state';
import { Selection, Transaction } from '@tiptap/pm/state';
import { Table } from '@tiptap/extension-table';
import { TableCell } from '@tiptap/extension-table-cell';
import { TableHeader } from '@tiptap/extension-table-header';
Expand Down Expand Up @@ -43,7 +41,16 @@ export const UmbTableHeader = TableHeader.extend({
},

addProseMirrorPlugins() {
const { editor } = this;
return [
UmbBubbleMenuPlugin(this.editor, {
unique: 'table-column-menu',
placement: 'top',
elementName: 'umb-tiptap-table-column-menu',
shouldShow(props) {
return isColumnGripSelected(props);
},
}),
new Plugin({
props: {
decorations: (state) => {
Expand All @@ -62,16 +69,16 @@ export const UmbTableHeader = TableHeader.extend({
decorations.push(
Decoration.widget(pos + 1, () => {
const colSelected = isColumnSelected(index)(selection);
const className = colSelected ? 'grip-column selected' : 'grip-column';

const grip = document.createElement('a');
grip.appendChild(document.createElement('uui-symbol-more'));

grip.className = className;
grip.className = colSelected ? 'grip-column selected' : 'grip-column';
grip.setAttribute('popovertarget', colSelected ? 'table-column-menu' : '');

grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();

this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr));
});

Expand Down Expand Up @@ -126,7 +133,16 @@ export const UmbTableCell = TableCell.extend({
},

addProseMirrorPlugins() {
const { editor } = this;
return [
UmbBubbleMenuPlugin(this.editor, {
unique: 'table-row-menu',
placement: 'left',
elementName: 'umb-tiptap-table-row-menu',
shouldShow(props) {
return isRowGripSelected(props);
},
}),
new Plugin({
props: {
decorations: (state) => {
Expand All @@ -145,12 +161,13 @@ export const UmbTableCell = TableCell.extend({
decorations.push(
Decoration.widget(pos + 1, () => {
const rowSelected = isRowSelected(index)(selection);
const className = rowSelected ? 'grip-row selected' : 'grip-row';

const grip = document.createElement('a');
grip.appendChild(document.createElement('uui-symbol-more'));

grip.className = className;
grip.className = rowSelected ? 'grip-row selected' : 'grip-row';
grip.setAttribute('popovertarget', rowSelected ? 'table-row-menu' : '');

grip.addEventListener('mousedown', (event) => {
event.preventDefault();
event.stopImmediatePropagation();
Expand Down Expand Up @@ -449,7 +466,7 @@ const isColumnGripSelected = ({
return !!gripColumn;
};

export const isRowGripSelected = ({
const isRowGripSelected = ({
editor,
view,
state,
Expand Down
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export * from './extensions/tiptap-html-global-attributes.extension.js';
export * from './extensions/tiptap-text-direction-extension.js';
export * from './extensions/tiptap-text-indent-extension.js';
export * from './extensions/tiptap-trailing-node.extension.js';
export * from './extensions/tiptap-umb-bubble-menu.extension.js';
export * from './extensions/tiptap-umb-embedded-media.extension.js';
export * from './extensions/tiptap-umb-image.extension.js';
export * from './extensions/tiptap-umb-link.extension.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement {
:host {
--uui-menu-item-flat-structure: 1;

background: var(--uui-color-surface);
background-color: var(--uui-color-surface);
border-radius: var(--uui-border-radius);
box-shadow: var(--uui-shadow-depth-3);
padding: var(--uui-size-space-1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase i
* @see {ManifestTiptapToolbarExtension}
* @param {Editor} editor The editor instance.
*/

public abstract execute(editor?: Editor): void;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,12 +645,11 @@ const toolbarExtensions: Array<UmbExtensionManifest> = [
},
];

const extensions = [
export const manifests = [
...kinds,
...coreExtensions,
...toolbarExtensions,
...blockExtensions,
...styleSelectExtensions,
...tableExtensions,
];

export const manifests = [...kinds, ...extensions];
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { Editor, UmbTiptapBubbleMenuElement } from '@umbraco-cms/backoffice/external/tiptap';

@customElement('umb-tiptap-table-column-menu')
export class UmbTiptapTableColumnMenuElement extends UmbLitElement implements UmbTiptapBubbleMenuElement {
@property({ attribute: false })
editor?: Editor;

#onAddColumnBefore = () => this.editor?.chain().focus().addColumnBefore().run();
#onAddColumnAfter = () => this.editor?.chain().focus().addColumnAfter().run();
#onDeleteColumn = () => this.editor?.chain().focus().deleteColumn().run();

override render() {
return html`
<uui-menu-item label="Add column before" @click=${this.#onAddColumnBefore}>
<uui-icon slot="icon" name="icon-navigation-first"></uui-icon>
</uui-menu-item>
<uui-menu-item label="Add column after" @click=${this.#onAddColumnAfter}>
<uui-icon slot="icon" name="icon-tab-key"></uui-icon>
</uui-menu-item>
<uui-menu-item label="Delete column" @click=${this.#onDeleteColumn}>
<uui-icon slot="icon" name="icon-trash"></uui-icon>
</uui-menu-item>
`;
}

static override readonly styles = [
css`
:host {
--uui-menu-item-flat-structure: 1;

display: flex;
flex-direction: column;

background-color: var(--uui-color-surface);
border-radius: var(--uui-border-radius);
box-shadow: var(--uui-shadow-depth-3);
}
`,
];
}

export default UmbTiptapTableColumnMenuElement;

declare global {
interface HTMLElementTagNameMap {
'umb-tiptap-table-column-menu': UmbTiptapTableColumnMenuElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { Editor, UmbTiptapBubbleMenuElement } from '@umbraco-cms/backoffice/external/tiptap';

@customElement('umb-tiptap-table-row-menu')
export class UmbTiptapTableRowMenuElement extends UmbLitElement implements UmbTiptapBubbleMenuElement {
@property({ attribute: false })
editor?: Editor;

#onAddRowBefore = () => this.editor?.chain().focus().addRowBefore().run();
#onAddRowAfter = () => this.editor?.chain().focus().addRowAfter().run();
#onDeleteRow = () => this.editor?.chain().focus().deleteRow().run();

override render() {
return html`
<uui-menu-item label="Add row before" @click=${this.#onAddRowBefore}>
<uui-icon slot="icon" name="icon-page-up"></uui-icon>
</uui-menu-item>
<uui-menu-item label="Add row after" @click=${this.#onAddRowAfter}>
<uui-icon slot="icon" name="icon-page-down"></uui-icon>
</uui-menu-item>
<uui-menu-item label="Delete row" @click=${this.#onDeleteRow}>
<uui-icon slot="icon" name="icon-trash"></uui-icon>
</uui-menu-item>
`;
}

static override readonly styles = [
css`
:host {
--uui-menu-item-flat-structure: 1;

display: flex;
flex-direction: column;

background-color: var(--uui-color-surface);
border-radius: var(--uui-border-radius);
box-shadow: var(--uui-shadow-depth-3);
}
`,
];
}

export default UmbTiptapTableRowMenuElement;

declare global {
interface HTMLElementTagNameMap {
'umb-tiptap-table-row-menu': UmbTiptapTableRowMenuElement;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,17 @@ const toolbarExtensions: Array<UmbExtensionManifest> = [
{
label: 'Row',
items: [
{ label: 'Add row before', data: 'addRowBefore' },
{ label: 'Add row after', data: 'addRowAfter' },
{ label: 'Add row before', icon: 'icon-page-up', data: 'addRowBefore' },
{ label: 'Add row after', icon: 'icon-page-down', data: 'addRowAfter' },
{ label: 'Delete row', icon: 'icon-trash', data: 'deleteRow' },
{ label: 'Toggle header row', data: 'toggleHeaderRow' },
],
},
{
label: 'Column',
items: [
{ label: 'Add column before', data: 'addColumnBefore' },
{ label: 'Add column after', data: 'addColumnAfter' },
{ label: 'Add column before', icon: 'icon-navigation-first', data: 'addColumnBefore' },
{ label: 'Add column after', icon: 'icon-tab-key', data: 'addColumnAfter' },
{ label: 'Delete column', icon: 'icon-trash', data: 'deleteColumn' },
{ label: 'Toggle header column', data: 'toggleHeaderColumn' },
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { UmbTiptapExtensionApiBase } from '../base.js';
import { css } from '@umbraco-cms/backoffice/external/lit';
import { UmbTable, UmbTableHeader, UmbTableRow, UmbTableCell } from '@umbraco-cms/backoffice/external/tiptap';

import './components/table-column-menu.element.js';
import './components/table-row-menu.element.js';

export default class UmbTiptapTableExtensionApi extends UmbTiptapExtensionApiBase {
getTiptapExtensions = () => [UmbTable, UmbTableHeader, UmbTableRow, UmbTableCell];

Expand Down
Loading
Loading