diff --git a/playwright/e2e/create-table-api.spec.ts b/playwright/e2e/create-table-api.spec.ts new file mode 100644 index 00000000000..cf6620ccbbe --- /dev/null +++ b/playwright/e2e/create-table-api.spec.ts @@ -0,0 +1,87 @@ +/** + * 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 randomUserTest } from '../support/fixtures/random-user' +import { test as uploadFileTest } from '../support/fixtures/upload-file' + +const test = mergeTests(uploadFileTest, randomUserTest) + +test.describe('createTable API', () => { + test.beforeEach(async ({ open, page }) => { + await open() + + // Load the editor API bundle + await page.addScriptTag({ + url: '/apps/text/js/text-editor.mjs', + type: 'module', + }) + }) + + 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') + }) +}) diff --git a/src/components/Editor/PlainTableContentEditor.vue b/src/components/Editor/PlainTableContentEditor.vue new file mode 100644 index 00000000000..520376d5893 --- /dev/null +++ b/src/components/Editor/PlainTableContentEditor.vue @@ -0,0 +1,136 @@ + + + + + + + diff --git a/src/editor.js b/src/editor.js index 536cfdccb28..4c5506377ad 100644 --- a/src/editor.js +++ b/src/editor.js @@ -281,3 +281,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..4240dae36bf --- /dev/null +++ b/src/extensions/PlainTable.js @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { Extension } from '@tiptap/core' + +import EditableTable from './../nodes/EditableTable.js' +import PlainTableDocument from './../nodes/PlainTableDocument.js' +import Keymap from './Keymap.js' +import Markdown from './Markdown.js' +/* eslint-disable import/no-named-as-default */ +import Text from '@tiptap/extension-text' + +export default Extension.create({ + name: 'PlainTable', + + addExtensions() { + return [Markdown, PlainTableDocument, EditableTable, Keymap, Text] + }, +}) diff --git a/src/extensions/index.js b/src/extensions/index.js index ae56e067906..800d9685324 100644 --- a/src/extensions/index.js +++ b/src/extensions/index.js @@ -9,6 +9,7 @@ import FocusTrap from './FocusTrap.js' import KeepSyntax from './KeepSyntax.js' import Markdown from './Markdown.js' import Mention from './Mention.js' +import PlainTable from './PlainTable.js' import PlainText from './PlainText.js' import RichText from './RichText.js' import UserColor from './UserColor.js' @@ -20,6 +21,7 @@ export { KeepSyntax, Markdown, Mention, + PlainTable, PlainText, RichText, UserColor, 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', +})