diff --git a/etc/lime-elements.api.md b/etc/lime-elements.api.md index a79645741d..09f3116bc1 100644 --- a/etc/lime-elements.api.md +++ b/etc/lime-elements.api.md @@ -565,6 +565,8 @@ export namespace Components { // @alpha "customElements": CustomElementDefinition[]; "language": Languages; + // (undocumented) + "supportTables": boolean; // @alpha "triggerCharacters": TriggerCharacter[]; "value": string; @@ -668,6 +670,8 @@ export namespace Components { // @alpha "customElements": CustomElementDefinition[]; "disabled"?: boolean; + // @alpha + "enableTables"?: boolean; "helperText"?: string; "invalid"?: boolean; "label"?: string; @@ -1551,6 +1555,8 @@ namespace JSX_2 { "customElements"?: CustomElementDefinition[]; "language"?: Languages; "onChange"?: (event: LimelProsemirrorAdapterCustomEvent) => void; + // (undocumented) + "supportTables"?: boolean; // @alpha "triggerCharacters"?: TriggerCharacter[]; "value"?: string; @@ -1667,6 +1673,8 @@ namespace JSX_2 { // @alpha "customElements"?: CustomElementDefinition[]; "disabled"?: boolean; + // @alpha + "enableTables"?: boolean; "helperText"?: string; "invalid"?: boolean; "label"?: string; diff --git a/package-lock.json b/package-lock.json index 322b0f168d..7d74ba8449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "prosemirror-example-setup": "^1.2.3", "prosemirror-markdown": "^1.13.1", "prosemirror-schema-basic": "^1.2.3", + "prosemirror-tables": "^1.5.0", "puppeteer": "^19.11.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -13565,6 +13566,19 @@ "prosemirror-view": "^1.27.0" } }, + "node_modules/prosemirror-tables": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.5.0.tgz", + "integrity": "sha512-VMx4zlYWm7aBlZ5xtfJHpqa3Xgu3b7srV54fXYnXgsAcIGRqKSrhiK3f89omzzgaAgAtDOV4ImXnLKhVfheVNQ==", + "dev": true, + "dependencies": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, "node_modules/prosemirror-transform": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz", @@ -26788,6 +26802,19 @@ "prosemirror-view": "^1.27.0" } }, + "prosemirror-tables": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.5.0.tgz", + "integrity": "sha512-VMx4zlYWm7aBlZ5xtfJHpqa3Xgu3b7srV54fXYnXgsAcIGRqKSrhiK3f89omzzgaAgAtDOV4ImXnLKhVfheVNQ==", + "dev": true, + "requires": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, "prosemirror-transform": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.8.0.tgz", diff --git a/package.json b/package.json index 3af77efc0d..1dbc83d109 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "prosemirror-example-setup": "^1.2.3", "prosemirror-markdown": "^1.13.1", "prosemirror-schema-basic": "^1.2.3", + "prosemirror-tables": "^1.5.0", "puppeteer": "^19.11.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/components/text-editor/examples/text-editor-with-tables.tsx b/src/components/text-editor/examples/text-editor-with-tables.tsx new file mode 100644 index 0000000000..97f73db6d7 --- /dev/null +++ b/src/components/text-editor/examples/text-editor-with-tables.tsx @@ -0,0 +1,50 @@ +import { Component, h, State } from '@stencil/core'; +/** + * Text editor with tables (HTML mode only). + * + * When using the text editor in HTML mode, it is possible to paste and + * display tables in the text editor. + * Basic interaction with the table is supported, but you cannot do + * complex operations + */ +@Component({ + tag: 'limel-example-text-editor-with-tables', + shadow: true, +}) +export class TextEditorWithTablesExample { + @State() + private value: string = + '

Column1

Column2

Cell A1

Cell B1

Cell A2

Cell B2

'; + + @State() + private readonly = false; + + public render() { + return [ + , + + + , + , + ]; + } + + private setReadonly = (event: CustomEvent) => { + event.stopPropagation(); + this.readonly = event.detail; + }; + + private handleChange = (event: CustomEvent) => { + this.value = event.detail; + }; +} diff --git a/src/components/text-editor/prosemirror-adapter/plugins/table-plugin.ts b/src/components/text-editor/prosemirror-adapter/plugins/table-plugin.ts new file mode 100644 index 0000000000..b24408c537 --- /dev/null +++ b/src/components/text-editor/prosemirror-adapter/plugins/table-plugin.ts @@ -0,0 +1,31 @@ +import { tableNodes, tableEditing } from 'prosemirror-tables'; +import { Plugin } from 'prosemirror-state'; + +export const getTableEditingPlugins = (tablesEnabled: boolean): Plugin[] => { + if (tablesEnabled) { + return [tableEditing()]; + } + + return []; +}; + +const createStyleAttribute = (cssProperty: string) => ({ + default: null, + getFromDOM: (dom: HTMLElement) => dom.style[cssProperty] || null, + setDOMAttr: (value: string, attrs: Record) => { + if (value) { + attrs.style = (attrs.style || '') + `${cssProperty}: ${value};`; + } + }, +}); + +export const getTableNodes = () => { + return tableNodes({ + tableGroup: 'block', + cellContent: 'block+', + cellAttributes: { + background: createStyleAttribute('background-color'), + color: createStyleAttribute('color'), + }, + }); +}; diff --git a/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx b/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx index 0125cfbc2a..3e411575fd 100644 --- a/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx +++ b/src/components/text-editor/prosemirror-adapter/prosemirror-adapter.tsx @@ -44,6 +44,7 @@ import { CustomElementDefinition } from '../../../global/shared-types/custom-ele import { createNodeSpec } from '../utils/plugin-factory'; import { createTriggerPlugin } from './plugins/trigger/factory'; import { TriggerCharacter } from '../text-editor.types'; +import { getTableNodes, getTableEditingPlugins } from './plugins/table-plugin'; const DEBOUNCE_TIMEOUT = 300; @@ -100,6 +101,14 @@ export class ProsemirrorAdapter { @Prop() triggerCharacters: TriggerCharacter[] = []; + @Prop() + /** + * Will only work if 'contentType' is set to 'html' + * @private + * @alpha + */ + public supportTables: boolean = false; + @Element() private host: HTMLLimelTextEditorElement; @@ -304,6 +313,10 @@ export class ProsemirrorAdapter { }); nodes = addListNodes(nodes, 'paragraph block*', 'block'); + if (this.supportTables) { + nodes = nodes.append(getTableNodes()); + } + return new Schema({ nodes: nodes, marks: schema.spec.marks.append({ @@ -343,6 +356,7 @@ export class ProsemirrorAdapter { this.updateActiveActionBarItems, ), createActionBarInteractionPlugin(this.menuCommandFactory), + ...getTableEditingPlugins(this.supportTables), ], }); } diff --git a/src/components/text-editor/text-editor.tsx b/src/components/text-editor/text-editor.tsx index 02ec18c638..255a507836 100644 --- a/src/components/text-editor/text-editor.tsx +++ b/src/components/text-editor/text-editor.tsx @@ -18,6 +18,7 @@ import { TriggerCharacter, TriggerEventDetail } from './text-editor.types'; * @exampleComponent limel-example-text-editor-as-form-component * @exampleComponent limel-example-text-editor-with-markdown * @exampleComponent limel-example-text-editor-with-html + * @exampleComponent limel-example-text-editor-with-tables * @exampleComponent limel-example-text-editor-allow-resize * @exampleComponent limel-example-text-editor-size * @exampleComponent limel-example-text-editor-ui @@ -153,6 +154,14 @@ export class TextEditor implements FormComponent { @Prop({ reflect: true }) public ui?: 'standard' | 'minimal' = 'standard'; + /** + * Set to `true` to allow parsing of table data. Only works when `type` is `html`. + * @private + * @alpha + */ + @Prop({ reflect: true }) + public enableTables?: boolean; + /** * Dispatched when a change is made to the editor */ @@ -246,12 +255,17 @@ export class TextEditor implements FormComponent { aria-disabled={this.disabled} language={this.language} triggerCharacters={this.triggers} + supportTables={this.checkForTables()} />, this.renderPlaceholder(), this.renderHelperLine(), ]; } + private checkForTables() { + return this.enableTables && this.contentType === 'html'; + } + private renderLabel() { if (!this.label) { return;