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 @@ -463,6 +463,10 @@
display: none;
}

&[data-highlight="false"] .lexxy-editor__toolbar-dropdown--highlight {
display: none;
}

&[data-upload="file"] button[name="image"] {
display: none;
}
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<lexxy-editor permitted-attachment-types="application/vnd.basecamp.mention application/vnd.basecamp.opengraph-embed"></lexxy-editor>`.
- `richText`: Pass `false` to disable rich text editing.
- `highlight`: Color highlighting configuration. Pass `{ enabled: false }` (or simply `false`) to disable highlighting entirely (it is enabled by default). See [Highlighting](highlighting.md) for configuring the available colors.

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.

Expand Down
16 changes: 16 additions & 0 deletions docs/highlighting.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,19 @@ Lexxy.configure({
}
})
```

## Disabling highlighting

Pass `highlight.enabled: false` to disable color highlighting entirely. The toolbar control is hidden, the commands become inert, and existing highlight markup is reduced to plain text on load.

```javascript
Lexxy.configure({
default: {
highlight: { enabled: false }
}
})
```

Or per editor: `<lexxy-editor highlight='{"enabled":false}'></lexxy-editor>`.

A bare boolean is also accepted as a shorthand — `highlight: false` in a preset, or `<lexxy-editor highlight="false">`.
1 change: 1 addition & 0 deletions src/config/lexxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const presets = new Configuration({
upload: "both"
},
highlight: {
enabled: true,
buttons: {
color: range(1, 9).map(n => `var(--highlight-${n})`),
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),
Expand Down
3 changes: 3 additions & 0 deletions src/elements/dropdown/highlight.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const NO_STYLE = Symbol("no_style")

export class HighlightDropdown extends ToolbarDropdown {
editorReady() {
if (!this.editorElement.supportsHighlight) return

this.#setUpButtons()
this.#registerButtonHandlers()
}
Expand All @@ -34,6 +36,7 @@ export class HighlightDropdown extends ToolbarDropdown {
this.#buttonContainer.innerHTML = ""

const colorGroups = this.editorElement.config.get("highlight.buttons")
if (!colorGroups) return

this.#populateButtonGroup("color", colorGroups.color)
this.#populateButtonGroup("background-color", colorGroups["background-color"])
Expand Down
48 changes: 46 additions & 2 deletions src/elements/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { buildEditorFromExtensions } from "@lexical/extension"
import { ListItemNode, ListNode, registerList } from "@lexical/list"
import { AutoLinkNode, LinkNode } from "@lexical/link"
import { $getNearestNodeOfType } from "@lexical/utils"
import { getCSSFromStyleObject, getStyleObjectFromCSS } from "@lexical/selection"
import { registerPlainText } from "@lexical/plain-text"
import { HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text"
import { $generateHtmlFromNodes, $generateNodesFromDOM as $generateLexicalNodesFromDOM } from "@lexical/html"
Expand Down Expand Up @@ -260,6 +261,13 @@ export class LexicalEditorElement extends HTMLElement {
return this.config.get("richText")
}

get supportsHighlight() {
// Accept both the object form ({ enabled: false }) and a bare boolean (highlight: false),
// mirroring the scalar-disable convention used by the sibling options.
const highlight = this.config.get("highlight")
return this.supportsRichText && highlight !== false && highlight?.enabled !== false
}

registerAdapter(adapter) {
this.adapter = adapter

Expand Down Expand Up @@ -407,6 +415,7 @@ export class LexicalEditorElement extends HTMLElement {
export: new Map([ [ TextNode, exportTextNodeDOM ], [ CodeHighlightNode, exportTextNodeDOM ] ])
},
$initialEditorState: (editor) => {
this.#removeDisabledConversions(editor)
this.#configureSanitizer(editor)
this.#loadInitialValue(editor)
this.#setInternalFormValue(this.#readSanitizedEditorValue(editor))
Expand Down Expand Up @@ -576,6 +585,7 @@ export class LexicalEditorElement extends HTMLElement {
registerRichText(this.editor),
registerList(this.editor)
)
this.#registerDisabledHighlightStripper(registered)
this.#registerTableComponents()
this.#registerCodeHiglightingComponents()
if (this.supportsMarkdown) {
Expand Down Expand Up @@ -716,6 +726,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-highlight", this.supportsHighlight) // Drives toolbar CSS styles
toolbar.configure(this.config.get("toolbar"))
this.prepend(toolbar)
return toolbar
Expand All @@ -725,6 +736,37 @@ export class LexicalEditorElement extends HTMLElement {
this.classList.toggle("lexxy-editor--empty", this.isEmpty)
}

// Highlight is stored as color/background-color styles (plus the highlight format bit)
// on text nodes. The <mark> import conversion handles HTML, but content pasted from
// another Lexxy editor arrives as serialized nodes whose styles are restored directly —
// bypassing that conversion. This transform clears the highlight styling so a disabled
// highlight can never render in the editor, whatever path produced it.
#registerDisabledHighlightStripper(registered) {
if (this.supportsHighlight) return

registered.push(this.editor.registerNodeTransform(TextNode, (node) => {
if (node.hasFormat("highlight")) node.toggleFormat("highlight")

const styles = getStyleObjectFromCSS(node.getStyle())
if (styles.color || styles["background-color"]) {
delete styles.color
delete styles["background-color"]
node.setStyle(getCSSFromStyleObject(styles))
}
}))
}

// The highlight extension owns the <mark> import conversion, but Lexical's TextNode
// also imports <mark> as its built-in highlight format. When highlight is disabled the
// extension isn't registered, so drop the conversion entirely — highlighted markup is
// then reduced to plain text on every import path (initial value, setValue, paste) and
// <mark> is excluded from the sanitizer allow-list, which derives from these keys.
#removeDisabledConversions(editor) {
if (this.supportsHighlight) return

editor._htmlConversions?.delete("mark")
}

#configureSanitizer(editor) {
setSanitizerConfig(this.#getAllowedElements(editor))
}
Expand Down Expand Up @@ -759,7 +801,7 @@ export class LexicalEditorElement extends HTMLElement {
italic: { active: format.isItalic, enabled: true },
strikethrough: { active: format.isStrikethrough, enabled: true },
code: { active: format.isInCode, enabled: true },
highlight: { active: format.isHighlight, enabled: true },
highlight: { active: this.supportsHighlight && format.isHighlight, enabled: this.supportsHighlight },
link: { active: format.isInLink, enabled: true },
Comment on lines 803 to 805

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — done. Gated both active and the separate highlight detail on supportsHighlight (not just enabled), so a disabled editor reports no highlight selection state to adapters regardless of any pre-existing or injected format.

quote: { active: format.isInQuote, enabled: true },
heading: { active: format.isInHeading, enabled: true },
Expand All @@ -770,7 +812,7 @@ export class LexicalEditorElement extends HTMLElement {
}

linkHref = linkNode ? linkNode.getURL() : null
highlight = format.isHighlight ? getHighlightStyles(selection) : null
highlight = this.supportsHighlight && format.isHighlight ? getHighlightStyles(selection) : null
headingTag = format.headingTag ?? null
})

Expand Down Expand Up @@ -804,6 +846,8 @@ export class LexicalEditorElement extends HTMLElement {
}

get #resolvedHighlightColors() {
if (!this.supportsHighlight) return null

const buttons = this.config.get("highlight.buttons")
if (!buttons) return null

Expand Down
2 changes: 1 addition & 1 deletion src/extensions/highlight_extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const pendingCodeHighlights = new WeakMap()

export class HighlightExtension extends LexxyExtension {
get enabled() {
return this.editorElement.supportsRichText
return this.editorElement.supportsHighlight
}

get lexicalExtension() {
Expand Down
20 changes: 15 additions & 5 deletions src/extensions/trix_content_extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,32 @@ export class TrixContentExtension extends LexxyExtension {
}

get lexicalExtension() {
// The em/span/strong/del converters below exist to import legacy Trix highlight
// colors. When highlight is disabled we drop the color application so those
// elements fall back to plain formatting (the em/span/strong handlers return null,
// deferring to Lexical's default bold/italic conversion; del keeps strikethrough).
const supportsHighlight = this.editorElement.supportsHighlight

return defineExtension({
name: "lexxy/trix-content",
html: {
import: {
em: (element) => onlyStyledElements(element, {
em: (element) => onlyStyledElements(element, supportsHighlight, {
conversion: extendTextNodeConversion("i", $applyHighlightStyle),
priority: 1
}),
span: (element) => onlyStyledElements(element, {
span: (element) => onlyStyledElements(element, supportsHighlight, {
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
priority: 1
}),
strong: (element) => onlyStyledElements(element, {
strong: (element) => onlyStyledElements(element, supportsHighlight, {
conversion: extendTextNodeConversion("b", $applyHighlightStyle),
priority: 1
}),
del: () => ({
conversion: extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle),
conversion: supportsHighlight
? extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle)
: extendTextNodeConversion("s", $applyStrikethrough),
priority: 1
}),
pre: (element) => onlyPreLanguageElements(element, {
Expand All @@ -43,7 +51,9 @@ export class TrixContentExtension extends LexxyExtension {
}
}

function onlyStyledElements(element, conversion) {
function onlyStyledElements(element, supportsHighlight, conversion) {
if (!supportsHighlight) return null

const elementHighlighted = element.style.color !== "" || element.style.backgroundColor !== ""
return elementHighlighted ? conversion : null
}
Expand Down
24 changes: 24 additions & 0 deletions test/browser/fixtures/highlight-false-bare.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 — Highlight Disabled (bare boolean)</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..." highlight="false" required></lexxy-editor>
</div>

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

<script type="module" src="/editor.js"></script>
</body>
</html>
24 changes: 24 additions & 0 deletions test/browser/fixtures/highlight-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 — Highlight 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..." highlight='{"enabled":false}' required></lexxy-editor>
</div>

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

<script type="module" src="/editor.js"></script>
</body>
</html>
Loading