From 9697ac5dfae570c7c766b75e60a15082af58adfd Mon Sep 17 00:00:00 2001 From: silver Date: Mon, 1 Dec 2025 17:47:12 +0100 Subject: [PATCH 1/8] feat: Add table-only editor API Add createTable() API to enable embedding markdown tables in other apps without rich text editing features. Introduces PlainTableDocument node that restricts content to tables only, and PlainTable extension that bundles required functionality (table editing, markdown serialization). - Add PlainTableDocument node with table-only content model - Add PlainTable extension (bundles Markdown, EditableTable, etc.) - Add PlainTableContentEditor.vue component - Expose window.OCA.Text.createTable() API in editor.js - Export PlainTable from extensions/index.js Signed-off-by: silver --- .../Editor/PlainTableContentEditor.vue | 142 ++++++++++++++++++ src/editor.js | 44 ++++++ src/extensions/PlainTable.js | 33 ++++ src/nodes/PlainTableDocument.js | 11 ++ 4 files changed, 230 insertions(+) create mode 100644 src/components/Editor/PlainTableContentEditor.vue create mode 100644 src/extensions/PlainTable.js create mode 100644 src/nodes/PlainTableDocument.js diff --git a/src/components/Editor/PlainTableContentEditor.vue b/src/components/Editor/PlainTableContentEditor.vue new file mode 100644 index 00000000000..82476d0dc58 --- /dev/null +++ b/src/components/Editor/PlainTableContentEditor.vue @@ -0,0 +1,142 @@ + + + + + + + diff --git a/src/editor.js b/src/editor.js index bae2c0da060..37e6032ca1e 100644 --- a/src/editor.js +++ b/src/editor.js @@ -269,3 +269,47 @@ window.OCA.Text.createEditor = async function({ .onSearch(onSearch) .render(el) } + +window.OCA.Text.createTable = async function ({ + // Element to render the editor to + el, + + content = '', + + readOnly = false, + autofocus = true, + + onCreate = ({ markdown }) => {}, + onLoaded = () => {}, + onUpdate = ({ markdown }) => {}, +}) { + const { default: PlainTableContentEditor } = await import( + './components/Editor/PlainTableContentEditor.vue' + ) + + const data = Vue.observable({ + readOnly, + content, + }) + + const vm = new Vue({ + data() { + return data + }, + render: (h) => { + return h(PlainTableContentEditor, { + props: { + content: data.content, + readOnly: data.readOnly, + showOutlineOutside: false, + }, + }) + }, + }) + + return new TextEditorEmbed(vm, data) + .onCreate(onCreate) + .onLoaded(onLoaded) + .onUpdate(onUpdate) + .render(el) +} diff --git a/src/extensions/PlainTable.js b/src/extensions/PlainTable.js new file mode 100644 index 00000000000..615c9c824aa --- /dev/null +++ b/src/extensions/PlainTable.js @@ -0,0 +1,33 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { Extension } from '@tiptap/core' + +import PlainTableDocument from './../nodes/PlainTableDocument.js' +import EditableTable from './../nodes/EditableTable.js' +import Markdown from './Markdown.js' +import Keymap from './Keymap.js' +/* eslint-disable import/no-named-as-default */ +import Text from '@tiptap/extension-text' + +export default Extension.create({ + name: 'PlainTable', + + addOptions() { + return { + ...this.parent?.(), + } + }, + + addExtensions() { + return [ + Markdown, + PlainTableDocument, + EditableTable, + Keymap, + Text, + ] + }, +}) diff --git a/src/nodes/PlainTableDocument.js b/src/nodes/PlainTableDocument.js new file mode 100644 index 00000000000..6564d3639f9 --- /dev/null +++ b/src/nodes/PlainTableDocument.js @@ -0,0 +1,11 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { Node } from '@tiptap/core' + +export default Node.create({ + name: 'doc', + content: 'table', +}) From c23216dca2f7dc9e1024d6f291c86d19f15d52e4 Mon Sep 17 00:00:00 2001 From: silver Date: Thu, 4 Dec 2025 09:09:34 +0100 Subject: [PATCH 2/8] remove obsolete addOptions from PlainTable extension Signed-off-by: silver --- src/extensions/PlainTable.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/extensions/PlainTable.js b/src/extensions/PlainTable.js index 615c9c824aa..acc1207f845 100644 --- a/src/extensions/PlainTable.js +++ b/src/extensions/PlainTable.js @@ -15,12 +15,6 @@ import Text from '@tiptap/extension-text' export default Extension.create({ name: 'PlainTable', - addOptions() { - return { - ...this.parent?.(), - } - }, - addExtensions() { return [ Markdown, From 5ec0e66eae876dcafb30184148ee75ba6db04326 Mon Sep 17 00:00:00 2001 From: silver Date: Thu, 4 Dec 2025 09:10:05 +0100 Subject: [PATCH 3/8] test createTable Signed-off-by: silver --- playwright/e2e/create-table-api.spec.ts | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 playwright/e2e/create-table-api.spec.ts diff --git a/playwright/e2e/create-table-api.spec.ts b/playwright/e2e/create-table-api.spec.ts new file mode 100644 index 00000000000..14fc6e2df26 --- /dev/null +++ b/playwright/e2e/create-table-api.spec.ts @@ -0,0 +1,88 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, mergeTests } from '@playwright/test' +import { test as uploadFileTest } from '../support/fixtures/upload-file' +import { test as randomUserTest } from '../support/fixtures/random-user' + +const test = mergeTests(uploadFileTest, randomUserTest) + +test.describe('createTable API', () => { + test.beforeEach(async ({ open, page }) => { + await open() + + // Load the editor API bundle + await page.evaluate(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Dynamic import in browser context + await import('/apps/text/js/text-editor.mjs') + }) + }) + + test('renders table editor', async ({ page }) => { + await page.evaluate(async () => { + const container = document.createElement('div') + container.id = 'test-table' + document.body.appendChild(container) + + // @ts-expect-error - OCA.Text is a global + await window.OCA.Text.createTable({ + el: container, + content: '| A | B |\n|---|---|\n| 1 | 2 |', + readOnly: false, + }) + }) + + await expect(page.locator('#test-table table')).toBeVisible() + await expect(page.locator('#test-table th').first()).toContainText('A') + await expect(page.locator('#test-table td').first()).toContainText('1') + }) + + test('allows editing when not readonly', async ({ page }) => { + await page.evaluate(async () => { + const container = document.createElement('div') + container.id = 'test-editable' + container.style.position = 'fixed' + container.style.top = '10px' + container.style.left = '10px' + container.style.zIndex = '10000' + container.style.background = 'white' + document.body.appendChild(container) + + // @ts-expect-error - OCA.Text is a global + await window.OCA.Text.createTable({ + el: container, + content: '| A |\n|---|\n| x |', + readOnly: false, + }) + }) + + const cell = page.locator('#test-editable .ProseMirror td').first() + await cell.click({ force: true }) + await page.waitForTimeout(100) + await cell.selectText() + await page.keyboard.type('edited') + + await expect(cell).toContainText('edited') + }) + + test('prevents editing when readonly', async ({ page }) => { + await page.evaluate(async () => { + const container = document.createElement('div') + container.id = 'test-readonly' + document.body.appendChild(container) + + // @ts-expect-error - OCA.Text is a global + await window.OCA.Text.createTable({ + el: container, + content: '| A |\n|---|---|\n| 1 |', + readOnly: true, + }) + }) + + const editable = page.locator('#test-readonly [contenteditable]').first() + await expect(editable).toHaveAttribute('contenteditable', 'false') + }) +}) From c3cc777e13afa86ab4216b0545df6ae20406a540 Mon Sep 17 00:00:00 2001 From: silver Date: Thu, 4 Dec 2025 11:37:53 +0100 Subject: [PATCH 4/8] make editorWith non-adjustable and disable file upload in editor Signed-off-by: silver --- src/components/Editor/PlainTableContentEditor.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Editor/PlainTableContentEditor.vue b/src/components/Editor/PlainTableContentEditor.vue index 82476d0dc58..2b26af4e455 100644 --- a/src/components/Editor/PlainTableContentEditor.vue +++ b/src/components/Editor/PlainTableContentEditor.vue @@ -22,7 +22,9 @@ import { UndoRedo } from '@tiptap/extensions' import { provide, watch } from 'vue' import { provideEditor } from '../../composables/useEditor.ts' import { editorFlagsKey } from '../../composables/useEditorFlags.ts' +import { editorWidthKey } from '../../composables/useEditorWidth.ts' import { useEditorMethods } from '../../composables/useEditorMethods.ts' +import { EDITOR_UPLOAD } from '../Editor.provider.ts' import { FocusTrap, PlainTable } from '../../extensions/index.js' import { createMarkdownSerializer } from '../../extensions/Markdown.js' import ContentContainer from './ContentContainer.vue' @@ -77,6 +79,8 @@ export default { isRichEditor: true, isRichWorkspace: false, }) + provide(editorWidthKey, null) + provide(EDITOR_UPLOAD, false) return { editor, setContent } }, From 49b827a9d8dad0c2e07788dfeb2f70f46ce87fd2 Mon Sep 17 00:00:00 2001 From: silver Date: Thu, 4 Dec 2025 15:18:06 +0100 Subject: [PATCH 5/8] refactor createTable test Signed-off-by: silver --- playwright/e2e/create-table-api.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/playwright/e2e/create-table-api.spec.ts b/playwright/e2e/create-table-api.spec.ts index 14fc6e2df26..f9b52e27ef6 100644 --- a/playwright/e2e/create-table-api.spec.ts +++ b/playwright/e2e/create-table-api.spec.ts @@ -14,10 +14,9 @@ test.describe('createTable API', () => { await open() // Load the editor API bundle - await page.evaluate(async () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Dynamic import in browser context - await import('/apps/text/js/text-editor.mjs') + await page.addScriptTag({ + url: '/apps/text/js/text-editor.mjs', + type: 'module', }) }) From c50ffd7a7eac4290fd698d226feb374536fcb8e7 Mon Sep 17 00:00:00 2001 From: silver Date: Thu, 4 Dec 2025 15:33:15 +0100 Subject: [PATCH 6/8] lint and refactor Signed-off-by: silver [skip ci] --- playwright/e2e/create-table-api.spec.ts | 2 +- .../Editor/PlainTableContentEditor.vue | 24 ++++++------------- src/extensions/PlainTable.js | 12 +++------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/playwright/e2e/create-table-api.spec.ts b/playwright/e2e/create-table-api.spec.ts index f9b52e27ef6..cf6620ccbbe 100644 --- a/playwright/e2e/create-table-api.spec.ts +++ b/playwright/e2e/create-table-api.spec.ts @@ -4,8 +4,8 @@ */ import { expect, mergeTests } from '@playwright/test' -import { test as uploadFileTest } from '../support/fixtures/upload-file' import { test as randomUserTest } from '../support/fixtures/random-user' +import { test as uploadFileTest } from '../support/fixtures/upload-file' const test = mergeTests(uploadFileTest, randomUserTest) diff --git a/src/components/Editor/PlainTableContentEditor.vue b/src/components/Editor/PlainTableContentEditor.vue index 2b26af4e455..520376d5893 100644 --- a/src/components/Editor/PlainTableContentEditor.vue +++ b/src/components/Editor/PlainTableContentEditor.vue @@ -4,9 +4,7 @@ -->