From b1af468aac16d144db5cd9d43b0eb8213a8e0427 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Wed, 10 Jun 2026 11:01:36 +0200 Subject: [PATCH] Add configurable tables support Make table insertion disablable via Lexxy.configure (tables: false) or a tables element attribute. The default stays true, preserving current behavior. When disabled: the TablesExtension is not registered (dropping the table nodes, table commands, and the figure/tbody import allow-list), the table toolbar button is hidden via the data-tables attribute, the lexxy-table-tools controls are not created, and insertTable no-ops. Table markup is reduced to plain text on import. --- app/assets/stylesheets/lexxy-editor.css | 4 ++ docs/configuration.md | 1 + src/config/lexxy.js | 1 + src/editor/command_dispatcher.js | 2 + src/elements/editor.js | 7 ++- src/extensions/tables_extension.js | 2 +- test/browser/fixtures/tables-false.html | 24 +++++++ test/browser/tests/tables/disabled.test.js | 73 ++++++++++++++++++++++ test/dummy/app/views/posts/_form.html.erb | 1 + test/system/tables_disabled_test.rb | 39 ++++++++++++ 10 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 test/browser/fixtures/tables-false.html create mode 100644 test/browser/tests/tables/disabled.test.js create mode 100644 test/system/tables_disabled_test.rb diff --git a/app/assets/stylesheets/lexxy-editor.css b/app/assets/stylesheets/lexxy-editor.css index a80e99d21..211bfd6fc 100644 --- a/app/assets/stylesheets/lexxy-editor.css +++ b/app/assets/stylesheets/lexxy-editor.css @@ -463,6 +463,10 @@ display: none; } + &[data-tables="false"] button[name="table"] { + display: none; + } + &[data-upload="file"] button[name="image"] { display: none; } diff --git a/docs/configuration.md b/docs/configuration.md index 07522f3ab..7b1844955 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -50,6 +50,7 @@ Editors support the following options, configurable using presets and element at - `multiLine`: Pass `false` to force single line editing. - `permittedAttachmentTypes`: Restrict the editor to a specific allowlist of attachment content types. Unset (the default) permits any content type. Example: ``. - `richText`: Pass `false` to disable rich text editing. +- `tables`: Pass `false` to disable tables entirely. Table insertion is removed, and any existing `` markup is reduced to plain text (cell text preserved) when loaded. By default, tables are enabled. The toolbar is considered part of the editor for `lexxy:focus` and `lexxy:blur` events. If the toolbar registers event or lexical handlers, it should expose a `dispose()` function which will be called on editor disconnect. diff --git a/src/config/lexxy.js b/src/config/lexxy.js index 4899b5c2c..5418feb2c 100644 --- a/src/config/lexxy.js +++ b/src/config/lexxy.js @@ -15,6 +15,7 @@ const presets = new Configuration({ multiLine: true, permittedAttachmentTypes: null, richText: true, + tables: true, toolbar: { upload: "both" }, diff --git a/src/editor/command_dispatcher.js b/src/editor/command_dispatcher.js index cd6c68e0d..9b526875c 100644 --- a/src/editor/command_dispatcher.js +++ b/src/editor/command_dispatcher.js @@ -277,6 +277,8 @@ export class CommandDispatcher { } dispatchInsertTable() { + if (!this.editorElement.supportsTables) return + this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true }) } diff --git a/src/elements/editor.js b/src/elements/editor.js index ea2161621..acdaf1390 100644 --- a/src/elements/editor.js +++ b/src/elements/editor.js @@ -260,6 +260,10 @@ export class LexicalEditorElement extends HTMLElement { return this.config.get("richText") } + get supportsTables() { + return this.supportsRichText && this.config.get("tables") + } + registerAdapter(adapter) { this.adapter = adapter @@ -576,7 +580,7 @@ export class LexicalEditorElement extends HTMLElement { registerRichText(this.editor), registerList(this.editor) ) - this.#registerTableComponents() + if (this.supportsTables) this.#registerTableComponents() this.#registerCodeHiglightingComponents() if (this.supportsMarkdown) { const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ] @@ -716,6 +720,7 @@ export class LexicalEditorElement extends HTMLElement { const toolbar = createElement("lexxy-toolbar") toolbar.innerHTML = LexicalToolbar.defaultTemplate toolbar.setAttribute("data-attachments", this.supportsAttachments) // Drives toolbar CSS styles + toolbar.setAttribute("data-tables", this.supportsTables) // Drives toolbar CSS styles toolbar.configure(this.config.get("toolbar")) this.prepend(toolbar) return toolbar diff --git a/src/extensions/tables_extension.js b/src/extensions/tables_extension.js index df14a2b26..cbebe5c5d 100644 --- a/src/extensions/tables_extension.js +++ b/src/extensions/tables_extension.js @@ -21,7 +21,7 @@ import { mergeRegister } from "@lexical/utils" export class TablesExtension extends LexxyExtension { get enabled() { - return this.editorElement.supportsRichText + return this.editorElement.supportsTables } get allowedElements() { diff --git a/test/browser/fixtures/tables-false.html b/test/browser/fixtures/tables-false.html new file mode 100644 index 000000000..b1e973fb3 --- /dev/null +++ b/test/browser/fixtures/tables-false.html @@ -0,0 +1,24 @@ + + + + + + Lexxy Test — Tables Disabled + + + + +
+ +
+ +
+ +
+ +
+ + + + + diff --git a/test/browser/tests/tables/disabled.test.js b/test/browser/tests/tables/disabled.test.js new file mode 100644 index 000000000..bc8368378 --- /dev/null +++ b/test/browser/tests/tables/disabled.test.js @@ -0,0 +1,73 @@ +import { test } from "../../test_helper.js" +import { expect } from "@playwright/test" +import { startMonitoringConsole } from "../../helpers/assertions.js" + +const TABLE_HTML = + '
alphabeta
gammadelta

After table

' + +const valueOf = async (editor) => { + await editor.flush() + return editor.value() +} + +test.describe("Tables disabled", () => { + test("the table toolbar button is present by default (regression baseline)", async ({ page }) => { + await page.goto("/") + await page.waitForSelector("lexxy-toolbar[connected]") + + await expect(page.locator("lexxy-toolbar button[name='table']")).toBeVisible() + }) + + test("the table toolbar button is hidden when tables are disabled", async ({ page }) => { + await page.goto("/tables-false.html") + await page.waitForSelector("lexxy-editor[connected]") + await page.waitForSelector("lexxy-toolbar[connected]") + + await expect(page.locator("lexxy-toolbar button[name='table']")).toBeHidden() + }) + + test("dispatching insertTable does nothing when tables are disabled", async ({ page, editor }) => { + await page.goto("/tables-false.html") + await editor.waitForConnected() + + await editor.click() + await editor.locator.evaluate((el) => el.editor.update(() => el.editor.dispatchCommand("insertTable"))) + + await expect(editor.content.locator("table")).toHaveCount(0) + await expect.poll(() => valueOf(editor)).not.toContain(" { + await page.goto("/tables-false.html") + await editor.waitForConnected() + + await expect(editor.locator.locator("lexxy-table-tools")).toHaveCount(0) + }) + + test("loading a table strips it to plain text when tables are disabled", async ({ page, editor }) => { + await page.goto("/tables-false.html") + await editor.waitForConnected() + + await editor.setValue(TABLE_HTML) + + await expect(editor.content.locator("table")).toHaveCount(0) + await expect.poll(() => valueOf(editor)).not.toContain(" valueOf(editor)).not.toContain("lexxy-content__table-wrapper") + await expect.poll(() => valueOf(editor)).toContain("After table") + + // strip-to-plain-text must preserve the cell content, not drop it + const value = await valueOf(editor) + for (const cell of [ "alpha", "beta", "gamma", "delta" ]) { + expect(value).toContain(cell) + } + }) + + test("editor connects without crashing when tables are disabled", async ({ page }) => { + startMonitoringConsole(page) + + await page.goto("/tables-false.html") + await page.waitForSelector("lexxy-editor[connected]") + + expect(page).toHaveNoErrors() + }) +}) diff --git a/test/dummy/app/views/posts/_form.html.erb b/test/dummy/app/views/posts/_form.html.erb index fc7e59dbc..992b4d638 100644 --- a/test/dummy/app/views/posts/_form.html.erb +++ b/test/dummy/app/views/posts/_form.html.erb @@ -43,6 +43,7 @@ markdown: params[:markdown_disabled] ? "false" : nil, "single-line": params[:multi_line_disabled] ? "true" : nil, "rich-text": params[:rich_text_disabled] ? "false" : nil, + tables: params[:tables_disabled] ? "false" : nil, toolbar: params[:toolbar_disabled] ? "false" : (params[:toolbar_external] ? "external_toolbar" : nil), data: (params[:authenticated_storage] == "true" ? { direct_upload_url: authenticated_direct_uploads_url } : {}), required: true do %> diff --git a/test/system/tables_disabled_test.rb b/test/system/tables_disabled_test.rb new file mode 100644 index 000000000..24c3e4ec6 --- /dev/null +++ b/test/system/tables_disabled_test.rb @@ -0,0 +1,39 @@ +require "application_system_test_case" + +class TablesDisabledTest < ApplicationSystemTestCase + setup do + visit edit_post_path(posts(:empty), tables_disabled: true) + wait_for_editor + end + + test "the table toolbar button is not visible" do + assert_no_selector "lexxy-toolbar button[name='table']" + end + + test "the lexxy-table-tools element is not created" do + assert_no_selector "lexxy-editor lexxy-table-tools" + end + + test "a saved table is stripped to plain text on load and round-trips without one" do + find_editor.value = '
alphabeta
gammadelta

After table

' + + assert_no_selector "lexxy-editor table" + assert_text "After table" + assert_text "alpha" + assert_text "delta" + + click_on "Update Post" + + within "article.post" do + assert_no_selector "table" + assert_text "After table" + assert_text "alpha" + assert_text "delta" + end + + click_on "Edit this post" + wait_for_editor + assert_no_match(/