Skip to content

Commit a84ca24

Browse files
committed
Add configurable highlight support
Make color highlighting disablable via Lexxy.configure (highlight: { enabled: false }) or a highlight element attribute. The default stays enabled, preserving current behavior. Both the object form { enabled: false } and a bare highlight: false are accepted. When disabled: the HighlightExtension (toggle/remove commands, the <mark> import converter, paste canonicalizers) is not registered, the toolbar highlight dropdown is hidden via a data-highlight attribute, the dropdown setup is guarded, and highlight markup is reduced to plain text on import — both <mark> (by dropping the conversion) and legacy Trix styled elements (em/strong/span/del keep their bold/italic/strikethrough but lose the highlight color).
1 parent d1c6a80 commit a84ca24

13 files changed

Lines changed: 307 additions & 8 deletions

File tree

app/assets/stylesheets/lexxy-editor.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,10 @@
463463
display: none;
464464
}
465465

466+
&[data-highlight="false"] .lexxy-editor__toolbar-dropdown--highlight {
467+
display: none;
468+
}
469+
466470
&[data-upload="file"] button[name="image"] {
467471
display: none;
468472
}

docs/configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Editors support the following options, configurable using presets and element at
5050
- `multiLine`: Pass `false` to force single line editing.
5151
- `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>`.
5252
- `richText`: Pass `false` to disable rich text editing.
53+
- `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.
5354

5455
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.
5556

docs/highlighting.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,19 @@ Lexxy.configure({
3232
}
3333
})
3434
```
35+
36+
## Disabling highlighting
37+
38+
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.
39+
40+
```javascript
41+
Lexxy.configure({
42+
default: {
43+
highlight: { enabled: false }
44+
}
45+
})
46+
```
47+
48+
Or per editor: `<lexxy-editor highlight='{"enabled":false}'></lexxy-editor>`.
49+
50+
A bare boolean is also accepted as a shorthand — `highlight: false` in a preset, or `<lexxy-editor highlight="false">`.

src/config/lexxy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const presets = new Configuration({
1919
upload: "both"
2020
},
2121
highlight: {
22+
enabled: true,
2223
buttons: {
2324
color: range(1, 9).map(n => `var(--highlight-${n})`),
2425
"background-color": range(1, 9).map(n => `var(--highlight-bg-${n})`),

src/elements/dropdown/highlight.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const NO_STYLE = Symbol("no_style")
1414

1515
export class HighlightDropdown extends ToolbarDropdown {
1616
editorReady() {
17+
if (!this.editorElement.supportsHighlight) return
18+
1719
this.#setUpButtons()
1820
this.#registerButtonHandlers()
1921
}
@@ -34,6 +36,7 @@ export class HighlightDropdown extends ToolbarDropdown {
3436
this.#buttonContainer.innerHTML = ""
3537

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

3841
this.#populateButtonGroup("color", colorGroups.color)
3942
this.#populateButtonGroup("background-color", colorGroups["background-color"])

src/elements/editor.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { buildEditorFromExtensions } from "@lexical/extension"
33
import { ListItemNode, ListNode, registerList } from "@lexical/list"
44
import { AutoLinkNode, LinkNode } from "@lexical/link"
55
import { $getNearestNodeOfType } from "@lexical/utils"
6+
import { getCSSFromStyleObject, getStyleObjectFromCSS } from "@lexical/selection"
67
import { registerPlainText } from "@lexical/plain-text"
78
import { HeadingNode, QuoteNode, registerRichText } from "@lexical/rich-text"
89
import { $generateHtmlFromNodes, $generateNodesFromDOM as $generateLexicalNodesFromDOM } from "@lexical/html"
@@ -260,6 +261,13 @@ export class LexicalEditorElement extends HTMLElement {
260261
return this.config.get("richText")
261262
}
262263

264+
get supportsHighlight() {
265+
// Accept both the object form ({ enabled: false }) and a bare boolean (highlight: false),
266+
// mirroring the scalar-disable convention used by the sibling options.
267+
const highlight = this.config.get("highlight")
268+
return this.supportsRichText && highlight !== false && highlight?.enabled !== false
269+
}
270+
263271
registerAdapter(adapter) {
264272
this.adapter = adapter
265273

@@ -407,6 +415,7 @@ export class LexicalEditorElement extends HTMLElement {
407415
export: new Map([ [ TextNode, exportTextNodeDOM ], [ CodeHighlightNode, exportTextNodeDOM ] ])
408416
},
409417
$initialEditorState: (editor) => {
418+
this.#removeDisabledConversions(editor)
410419
this.#configureSanitizer(editor)
411420
this.#loadInitialValue(editor)
412421
this.#setInternalFormValue(this.#readSanitizedEditorValue(editor))
@@ -576,6 +585,7 @@ export class LexicalEditorElement extends HTMLElement {
576585
registerRichText(this.editor),
577586
registerList(this.editor)
578587
)
588+
this.#registerDisabledHighlightStripper(registered)
579589
this.#registerTableComponents()
580590
this.#registerCodeHiglightingComponents()
581591
if (this.supportsMarkdown) {
@@ -716,6 +726,7 @@ export class LexicalEditorElement extends HTMLElement {
716726
const toolbar = createElement("lexxy-toolbar")
717727
toolbar.innerHTML = LexicalToolbar.defaultTemplate
718728
toolbar.setAttribute("data-attachments", this.supportsAttachments) // Drives toolbar CSS styles
729+
toolbar.setAttribute("data-highlight", this.supportsHighlight) // Drives toolbar CSS styles
719730
toolbar.configure(this.config.get("toolbar"))
720731
this.prepend(toolbar)
721732
return toolbar
@@ -725,6 +736,37 @@ export class LexicalEditorElement extends HTMLElement {
725736
this.classList.toggle("lexxy-editor--empty", this.isEmpty)
726737
}
727738

739+
// Highlight is stored as color/background-color styles (plus the highlight format bit)
740+
// on text nodes. The <mark> import conversion handles HTML, but content pasted from
741+
// another Lexxy editor arrives as serialized nodes whose styles are restored directly —
742+
// bypassing that conversion. This transform clears the highlight styling so a disabled
743+
// highlight can never render in the editor, whatever path produced it.
744+
#registerDisabledHighlightStripper(registered) {
745+
if (this.supportsHighlight) return
746+
747+
registered.push(this.editor.registerNodeTransform(TextNode, (node) => {
748+
if (node.hasFormat("highlight")) node.toggleFormat("highlight")
749+
750+
const styles = getStyleObjectFromCSS(node.getStyle())
751+
if (styles.color || styles["background-color"]) {
752+
delete styles.color
753+
delete styles["background-color"]
754+
node.setStyle(getCSSFromStyleObject(styles))
755+
}
756+
}))
757+
}
758+
759+
// The highlight extension owns the <mark> import conversion, but Lexical's TextNode
760+
// also imports <mark> as its built-in highlight format. When highlight is disabled the
761+
// extension isn't registered, so drop the conversion entirely — highlighted markup is
762+
// then reduced to plain text on every import path (initial value, setValue, paste) and
763+
// <mark> is excluded from the sanitizer allow-list, which derives from these keys.
764+
#removeDisabledConversions(editor) {
765+
if (this.supportsHighlight) return
766+
767+
editor._htmlConversions?.delete("mark")
768+
}
769+
728770
#configureSanitizer(editor) {
729771
setSanitizerConfig(this.#getAllowedElements(editor))
730772
}
@@ -759,7 +801,7 @@ export class LexicalEditorElement extends HTMLElement {
759801
italic: { active: format.isItalic, enabled: true },
760802
strikethrough: { active: format.isStrikethrough, enabled: true },
761803
code: { active: format.isInCode, enabled: true },
762-
highlight: { active: format.isHighlight, enabled: true },
804+
highlight: { active: this.supportsHighlight && format.isHighlight, enabled: this.supportsHighlight },
763805
link: { active: format.isInLink, enabled: true },
764806
quote: { active: format.isInQuote, enabled: true },
765807
heading: { active: format.isInHeading, enabled: true },
@@ -770,7 +812,7 @@ export class LexicalEditorElement extends HTMLElement {
770812
}
771813

772814
linkHref = linkNode ? linkNode.getURL() : null
773-
highlight = format.isHighlight ? getHighlightStyles(selection) : null
815+
highlight = this.supportsHighlight && format.isHighlight ? getHighlightStyles(selection) : null
774816
headingTag = format.headingTag ?? null
775817
})
776818

@@ -804,6 +846,8 @@ export class LexicalEditorElement extends HTMLElement {
804846
}
805847

806848
get #resolvedHighlightColors() {
849+
if (!this.supportsHighlight) return null
850+
807851
const buttons = this.config.get("highlight.buttons")
808852
if (!buttons) return null
809853

src/extensions/highlight_extension.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const pendingCodeHighlights = new WeakMap()
2424

2525
export class HighlightExtension extends LexxyExtension {
2626
get enabled() {
27-
return this.editorElement.supportsRichText
27+
return this.editorElement.supportsHighlight
2828
}
2929

3030
get lexicalExtension() {

src/extensions/trix_content_extension.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,32 @@ export class TrixContentExtension extends LexxyExtension {
1313
}
1414

1515
get lexicalExtension() {
16+
// The em/span/strong/del converters below exist to import legacy Trix highlight
17+
// colors. When highlight is disabled we drop the color application so those
18+
// elements fall back to plain formatting (the em/span/strong handlers return null,
19+
// deferring to Lexical's default bold/italic conversion; del keeps strikethrough).
20+
const supportsHighlight = this.editorElement.supportsHighlight
21+
1622
return defineExtension({
1723
name: "lexxy/trix-content",
1824
html: {
1925
import: {
20-
em: (element) => onlyStyledElements(element, {
26+
em: (element) => onlyStyledElements(element, supportsHighlight, {
2127
conversion: extendTextNodeConversion("i", $applyHighlightStyle),
2228
priority: 1
2329
}),
24-
span: (element) => onlyStyledElements(element, {
30+
span: (element) => onlyStyledElements(element, supportsHighlight, {
2531
conversion: extendTextNodeConversion("mark", $applyHighlightStyle),
2632
priority: 1
2733
}),
28-
strong: (element) => onlyStyledElements(element, {
34+
strong: (element) => onlyStyledElements(element, supportsHighlight, {
2935
conversion: extendTextNodeConversion("b", $applyHighlightStyle),
3036
priority: 1
3137
}),
3238
del: () => ({
33-
conversion: extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle),
39+
conversion: supportsHighlight
40+
? extendTextNodeConversion("s", $applyStrikethrough, $applyHighlightStyle)
41+
: extendTextNodeConversion("s", $applyStrikethrough),
3442
priority: 1
3543
}),
3644
pre: (element) => onlyPreLanguageElements(element, {
@@ -43,7 +51,9 @@ export class TrixContentExtension extends LexxyExtension {
4351
}
4452
}
4553

46-
function onlyStyledElements(element, conversion) {
54+
function onlyStyledElements(element, supportsHighlight, conversion) {
55+
if (!supportsHighlight) return null
56+
4757
const elementHighlighted = element.style.color !== "" || element.style.backgroundColor !== ""
4858
return elementHighlighted ? conversion : null
4959
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1">
6+
<title>Lexxy Test — Highlight Disabled (bare boolean)</title>
7+
<link rel="stylesheet" href="/styles.css">
8+
</head>
9+
<body>
10+
<form>
11+
<div class="title">
12+
<input type="text" name="post[title]" placeholder="Post title" aria-label="Post title">
13+
</div>
14+
15+
<div class="body">
16+
<lexxy-editor class="lexxy-content" placeholder="Write something..." highlight="false" required></lexxy-editor>
17+
</div>
18+
19+
<div class="events"></div>
20+
</form>
21+
22+
<script type="module" src="/editor.js"></script>
23+
</body>
24+
</html>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1">
6+
<title>Lexxy Test — Highlight Disabled</title>
7+
<link rel="stylesheet" href="/styles.css">
8+
</head>
9+
<body>
10+
<form>
11+
<div class="title">
12+
<input type="text" name="post[title]" placeholder="Post title" aria-label="Post title">
13+
</div>
14+
15+
<div class="body">
16+
<lexxy-editor class="lexxy-content" placeholder="Write something..." highlight='{"enabled":false}' required></lexxy-editor>
17+
</div>
18+
19+
<div class="events"></div>
20+
</form>
21+
22+
<script type="module" src="/editor.js"></script>
23+
</body>
24+
</html>

0 commit comments

Comments
 (0)