Skip to content
Open
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
4 changes: 4 additions & 0 deletions app/assets/stylesheets/lexxy-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@
display: none;
}

&[data-tables="false"] button[name="table"] {
display: none;
}

&[data-upload="file"] button[name="image"] {
display: none;
}
Expand Down
1 change: 1 addition & 0 deletions home/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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: `<lexxy-editor permitted-attachment-types="application/vnd.basecamp.mention application/vnd.basecamp.opengraph-embed"></lexxy-editor>`.
- `richText`: Pass `false` to disable rich text editing.
- `tables`: Pass `false` to disable tables entirely. Table insertion is removed, and any existing `<table>` markup is reduced to plain text (cell text preserved) when loaded. By default, tables are enabled.
- `headings`: Pass an array of heading tags to configure which heading levels are available in the toolbar dropdown. Defaults to `["h2", "h3", "h4"]`. Pass an empty array to remove all heading options; the formatting dropdown still offers "Normal" and "Clear formatting".

```js
Expand Down
1 change: 1 addition & 0 deletions src/config/lexxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const presets = new Configuration({
multiLine: true,
permittedAttachmentTypes: null,
richText: true,
tables: true,
toolbar: {
upload: "both"
},
Expand Down
2 changes: 2 additions & 0 deletions src/editor/command_dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ export class CommandDispatcher {
}

dispatchInsertTable() {
if (!this.editorElement.supportsTables) return

this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true })
}

Expand Down
7 changes: 6 additions & 1 deletion src/elements/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,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

Expand Down Expand Up @@ -604,7 +608,7 @@ export class LexicalEditorElement extends HTMLElement {
registerRichText(this.editor),
registerList(this.editor)
)
this.#registerTableComponents()
if (this.supportsTables) this.#registerTableComponents()
this.#registerCodeLanguagePicker()
if (this.supportsMarkdown) {
const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ]
Expand Down Expand Up @@ -743,6 +747,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"))
Comment on lines 747 to 751

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working as intended. The data-tables/data-attachments attributes drive button visibility for Lexxy's default toolbar template, which always ships those buttons. External (toolbar="id") and pre-rendered <lexxy-toolbar> toolbars are consumer-authored — the consumer controls which buttons exist, so if tables are disabled they simply omit the table button. This mirrors the existing data-attachments behavior. Actual disabling happens at the editor level regardless of toolbar: the table nodes/plugin are never registered and dispatchInsertTable() no-ops, so a stray button would be inert anyway.

this.prepend(toolbar)
return toolbar
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/tables_extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
24 changes: 24 additions & 0 deletions test/browser/fixtures/tables-false.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lexxy Test — Tables Disabled</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<form>
<div class="title">
<input type="text" name="post[title]" placeholder="Post title" aria-label="Post title">
</div>

<div class="body">
<lexxy-editor class="lexxy-content" placeholder="Write something..." tables="false" required></lexxy-editor>
</div>

<div class="events"></div>
</form>

<script type="module" src="/editor.js"></script>
</body>
</html>
73 changes: 73 additions & 0 deletions test/browser/tests/tables/disabled.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { test } from "../../test_helper.js"
import { expect } from "@playwright/test"
import { startMonitoringConsole } from "../../helpers/assertions.js"

const TABLE_HTML =
'<figure class="lexxy-content__table-wrapper"><table><thead><tr><th>alpha</th><th>beta</th></tr></thead><tbody><tr><td>gamma</td><td>delta</td></tr></tbody></table></figure><p>After table</p>'

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("<table")
Comment on lines +33 to +37

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"insertTable" is a registered Lexxy command — it is exactly what the toolbar button dispatches via data-command="insertTable" (see toolbar.js and the COMMANDS list in command_dispatcher.js), and it routes to INSERT_TABLE_COMMAND internally. So the test does exercise the real insert path. When tables are disabled the toolbar button is CSS-hidden (not removed), so dispatching that command programmatically is the appropriate mechanism here; the disabled behavior is additionally covered by the button-hidden and load-stripping tests in this file. Happy to switch the mechanism if you prefer.

})

test("the lexxy-table-tools element is not created when tables are disabled", async ({ page, editor }) => {
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("<table")
await expect.poll(() => 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()
})
})
1 change: 1 addition & 0 deletions test/dummy/app/views/posts/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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 %>
Expand Down
39 changes: 39 additions & 0 deletions test/system/tables_disabled_test.rb
Original file line number Diff line number Diff line change
@@ -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 = '<figure class="lexxy-content__table-wrapper"><table><thead><tr><th>alpha</th><th>beta</th></tr></thead><tbody><tr><td>gamma</td><td>delta</td></tr></tbody></table></figure><p>After table</p>'

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(/<table/, find_editor.value)
assert_includes find_editor.value, "alpha"
end
end