diff --git a/app/assets/stylesheets/lexxy-editor.css b/app/assets/stylesheets/lexxy-editor.css
index a80e99d21..ed558da03 100644
--- a/app/assets/stylesheets/lexxy-editor.css
+++ b/app/assets/stylesheets/lexxy-editor.css
@@ -463,6 +463,21 @@
display: none;
}
+ &[data-tables="false"] button[name="table"] {
+ display: none;
+ }
+
+ &[data-highlight="false"] .lexxy-editor__toolbar-dropdown--highlight {
+ display: none;
+ }
+
+ &[data-disabled-marks~="bold"] button[name="bold"],
+ &[data-disabled-marks~="italic"] button[name="italic"],
+ &[data-disabled-marks~="strikethrough"] button[name="strikethrough"],
+ &[data-disabled-marks~="underline"] button[name="underline"] {
+ display: none;
+ }
+
&[data-upload="file"] button[name="image"] {
display: none;
}
diff --git a/docs/configuration.md b/docs/configuration.md
index 07522f3ab..8872a74d1 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -47,9 +47,13 @@ Editors support the following options, configurable using presets and element at
- `toolbar.upload`: Control which upload button(s) appear in the toolbar. Accepts `"file"`, `"image"`, or `"both"` (default). The image button restricts the file picker to images and videos (`accept="image/*,video/*"`), which triggers the native photo/video picker on iOS and Android. The file button opens an unrestricted file picker.
- `attachments`: Pass `false` to disable attachments completely. By default, attachments are supported, including paste and drag & drop support. For finer-grained control — keeping attachments enabled while restricting which content types are accepted — use `permittedAttachmentTypes`.
- `markdown`: Pass `false` to disable Markdown support.
+- `marks`: Choose which inline marks the editor allows, as an allowlist. Defaults to `["bold", "italic", "strikethrough", "underline"]` (all). Any mark left out is disabled everywhere — its toolbar button is hidden, its keyboard shortcut and Markdown shortcut are inert, and its markup is reduced to plain text on import (paste, `value`, and initial content). Pass `[]` to disable all inline marks. Example: ``.
- `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.
+- `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.
+- `headings`: Choose which heading levels the toolbar offers, as an array of heading tags. Defaults to `["h2", "h3", "h4"]`. Any of `h1`–`h4` listed gets a button in the format dropdown; levels left out are hidden. Pass `[]` to remove every heading button. Markdown shortcuts (`#`…`######`) still produce headings regardless of this setting. Example: ``.
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/docs/highlighting.md b/docs/highlighting.md
index db50f4af8..4b183d4f5 100644
--- a/docs/highlighting.md
+++ b/docs/highlighting.md
@@ -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: ``.
+
+A bare boolean is also accepted as a shorthand — `highlight: false` in a preset, or ``.
diff --git a/src/config/lexxy.js b/src/config/lexxy.js
index 4899b5c2c..976e50ad2 100644
--- a/src/config/lexxy.js
+++ b/src/config/lexxy.js
@@ -15,10 +15,15 @@ const presets = new Configuration({
multiLine: true,
permittedAttachmentTypes: null,
richText: true,
+ tables: true,
+ code: true,
+ marks: [ "bold", "italic", "strikethrough", "underline" ],
+ headings: [ "h2", "h3", "h4" ],
toolbar: {
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})`),
diff --git a/src/editor/command_dispatcher.js b/src/editor/command_dispatcher.js
index cd6c68e0d..201b184f5 100644
--- a/src/editor/command_dispatcher.js
+++ b/src/editor/command_dispatcher.js
@@ -4,6 +4,7 @@ import {
$isRangeSelection,
$isTextNode,
$setSelection,
+ COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_NORMAL,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
@@ -33,6 +34,7 @@ const COMMANDS = [
"unlink",
"toggleHighlight",
"removeHighlight",
+ "setFormatHeadingHuge",
"setFormatHeadingLarge",
"setFormatHeadingMedium",
"setFormatHeadingSmall",
@@ -68,6 +70,7 @@ export class CommandDispatcher {
this.contents = editorElement.contents
this.#registerCommands()
+ this.#registerDisabledMarkInterceptor()
this.#registerKeyboardCommands()
this.#registerDragAndDropHandlers()
}
@@ -156,6 +159,8 @@ export class CommandDispatcher {
}
dispatchInsertCodeBlock() {
+ if (!this.editorElement.supportsCode) return
+
if (this.selection.hasSelectedWordsInSingleLine) {
this.#toggleInlineCode()
} else {
@@ -228,6 +233,10 @@ export class CommandDispatcher {
$insertNodeToNearestRoot(new HorizontalDividerNode)
}
+ dispatchSetFormatHeadingHuge() {
+ this.contents.applyHeadingFormat("h1")
+ }
+
dispatchSetFormatHeadingLarge() {
this.contents.applyHeadingFormat("h2")
}
@@ -277,6 +286,8 @@ export class CommandDispatcher {
}
dispatchInsertTable() {
+ if (!this.editorElement.supportsTables) return
+
this.editor.dispatchCommand(INSERT_TABLE_COMMAND, { "rows": 3, "columns": 3, "includeHeaders": true })
}
@@ -299,6 +310,23 @@ export class CommandDispatcher {
}
}
+ // Swallow FORMAT_TEXT_COMMAND for disabled marks at high priority, before
+ // Lexical's rich-text handler (registered at COMMAND_PRIORITY_EDITOR) can apply
+ // them. This single hook covers every path that ends in FORMAT_TEXT_COMMAND:
+ // the toolbar buttons, programmatic dispatch, and the native Cmd+B/I/U shortcuts
+ // Lexical dispatches internally even when the button is gone.
+ #registerDisabledMarkInterceptor() {
+ const disabledMarks = this.editorElement.disabledMarks
+ if (disabledMarks.length === 0) return
+
+ const disabled = new Set(disabledMarks)
+ this.#registerCommandHandler(
+ FORMAT_TEXT_COMMAND,
+ COMMAND_PRIORITY_HIGH,
+ (format) => disabled.has(format)
+ )
+ }
+
#registerCommandHandler(command, priority, handler) {
this.#listeners.track(this.editor.registerCommand(command, handler, priority))
}
diff --git a/src/editor/headings.js b/src/editor/headings.js
new file mode 100644
index 000000000..a5c812a3b
--- /dev/null
+++ b/src/editor/headings.js
@@ -0,0 +1,11 @@
+// Canonical heading levels the toolbar can offer, ordered largest to smallest.
+// The `name`/`command` for h2-h4 are the original public contract — referenced by
+// host adapters, the toolbar template, and the native `editor-initialized` event —
+// so they must not change. h1 adds a new name/command. Which of these levels are
+// actually offered is driven by the `headings` configuration option.
+export const HEADING_LEVELS = [
+ { tag: "h1", name: "heading-huge", command: "setFormatHeadingHuge", label: "Huge heading" },
+ { tag: "h2", name: "heading-large", command: "setFormatHeadingLarge", label: "Large heading" },
+ { tag: "h3", name: "heading-medium", command: "setFormatHeadingMedium", label: "Medium heading" },
+ { tag: "h4", name: "heading-small", command: "setFormatHeadingSmall", label: "Small heading" },
+]
diff --git a/src/editor/marks.js b/src/editor/marks.js
new file mode 100644
index 000000000..6485e0d22
--- /dev/null
+++ b/src/editor/marks.js
@@ -0,0 +1,32 @@
+// Inline marks that can be enabled or disabled through the `marks` option.
+// Frozen because the editor's enabledMarks getter can return it directly.
+export const MARK_TYPES = Object.freeze([ "bold", "italic", "strikethrough", "underline" ])
+
+// Semantic HTML tags each mark is imported from. Deleting these tags from the
+// editor's html conversions strips the mark on import: the default TextNode
+// conversion and the legacy Trix conversion share the same tag key, so dropping
+// the key removes both. (`del` is contributed by the Trix content extension; the
+// rest come from Lexical's TextNode.importDOM.)
+//
+// Note: because the Trix conversion under these keys also carries legacy highlight
+// color (e.g. ``), disabling a mark drops that co-located color
+// too — an accepted consequence of stripping a now-disabled feature on load.
+export const MARK_TO_TAGS = {
+ bold: [ "b", "strong" ],
+ italic: [ "i", "em" ],
+ strikethrough: [ "s", "del" ],
+ underline: [ "u" ]
+}
+
+// Drop the markdown transformers that would produce a disabled mark. Text-format
+// transformers expose a `format` array (e.g. ["bold"] or ["bold", "italic"]); a
+// combined transformer is dropped when either of its formats is disabled.
+// Transformers without a `format` (headings, lists, links…) are left untouched.
+export function withoutDisabledMarkTransformers(transformers, disabledMarks) {
+ if (disabledMarks.length === 0) return transformers
+
+ const disabled = new Set(disabledMarks)
+ return transformers.filter((transformer) =>
+ !Array.isArray(transformer.format) || !transformer.format.some((format) => disabled.has(format))
+ )
+}
diff --git a/src/elements/dropdown/highlight.js b/src/elements/dropdown/highlight.js
index 15e1026a1..ed55e2b19 100644
--- a/src/elements/dropdown/highlight.js
+++ b/src/elements/dropdown/highlight.js
@@ -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()
}
@@ -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"])
diff --git a/src/elements/editor.js b/src/elements/editor.js
index ea2161621..c0445c18d 100644
--- a/src/elements/editor.js
+++ b/src/elements/editor.js
@@ -3,15 +3,18 @@ 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"
import { filterDisallowedAttachmentNodes } from "../helpers/attachment_filter_helper"
import { $convertInlineImageDataURIs } from "../helpers/inline_image_uri_helper"
import { CodeHighlightNode, CodeNode, registerCodeHighlighting } from "@lexical/code"
-import { TRANSFORMERS, registerMarkdownShortcuts } from "@lexical/markdown"
+import { CODE, INLINE_CODE, TRANSFORMERS, registerMarkdownShortcuts } from "@lexical/markdown"
import { HORIZONTAL_DIVIDER } from "../editor/markdown/horizontal_divider_transformer"
import { registerMarkdownLeadingTagHandler } from "../editor/markdown/leading_tag_handler"
+import { MARK_TO_TAGS, MARK_TYPES, withoutDisabledMarkTransformers } from "../editor/marks"
+import { HEADING_LEVELS } from "../editor/headings"
import theme from "../config/theme"
import { HorizontalDividerNode } from "../nodes/horizontal_divider_node"
@@ -47,7 +50,6 @@ import { nextFrame } from "../helpers/timing_helper.js"
export class LexicalEditorElement extends HTMLElement {
static formAssociated = true
static debug = false
- static commands = [ "bold", "italic", "strikethrough" ]
static observedAttributes = [ "connected", "required" ]
@@ -260,6 +262,47 @@ export class LexicalEditorElement extends HTMLElement {
return this.config.get("richText")
}
+ get supportsTables() {
+ return this.supportsRichText && this.config.get("tables")
+ }
+
+ 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
+ }
+
+ get supportsCode() {
+ return this.supportsRichText && this.config.get("code")
+ }
+
+ // The inline marks the editor allows, as an allow-list intersected with the known
+ // mark types. Accepts an array (`marks='["bold"]'`) or a whitespace-separated string
+ // (`marks="bold italic"`), mirroring `permittedAttachmentTypes`. Any other value —
+ // a bare/empty attribute, a boolean, a number — is not a valid allow-list and falls
+ // back to all marks enabled, so a misconfiguration never silently disables everything.
+ // Use `marks='[]'` to disable every mark.
+ get enabledMarks() {
+ const configured = this.config.get("marks")
+
+ let list
+ if (Array.isArray(configured)) {
+ list = configured
+ } else if (typeof configured === "string" && configured.trim() !== "") {
+ list = configured.split(/\s+/)
+ } else {
+ return MARK_TYPES
+ }
+
+ return Object.freeze(MARK_TYPES.filter((mark) => list.includes(mark)))
+ }
+
+ get disabledMarks() {
+ const enabled = this.enabledMarks
+ return Object.freeze(MARK_TYPES.filter((mark) => !enabled.includes(mark)))
+ }
+
registerAdapter(adapter) {
this.adapter = adapter
@@ -407,6 +450,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))
@@ -435,12 +479,14 @@ export class LexicalEditorElement extends HTMLElement {
HeadingNode,
ListNode,
ListItemNode,
- CodeNode,
- CodeHighlightNode,
LinkNode,
AutoLinkNode,
HorizontalDividerNode
)
+
+ if (this.supportsCode) {
+ nodes.push(CodeNode, CodeHighlightNode)
+ }
}
return nodes
@@ -576,10 +622,15 @@ export class LexicalEditorElement extends HTMLElement {
registerRichText(this.editor),
registerList(this.editor)
)
- this.#registerTableComponents()
- this.#registerCodeHiglightingComponents()
+ this.#registerDisabledMarkStripper(registered)
+ this.#registerDisabledHighlightStripper(registered)
+ if (this.supportsTables) this.#registerTableComponents()
+ if (this.supportsCode) this.#registerCodeHiglightingComponents()
if (this.supportsMarkdown) {
- const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ]
+ // Both handlers must receive the disabled-mark-filtered list: the leading-tag handler
+ // applies formats via selection.formatText() (not FORMAT_TEXT_COMMAND), so the command
+ // interceptor can't stop it — dropping the transformer is what disables the shortcut there.
+ const transformers = withoutDisabledMarkTransformers(this.#enabledMarkdownTransformers(), this.disabledMarks)
registered.push(
registerMarkdownShortcuts(this.editor, transformers),
registerMarkdownLeadingTagHandler(this.editor, transformers)
@@ -592,6 +643,13 @@ export class LexicalEditorElement extends HTMLElement {
this.#listeners.track(...registered)
}
+ #enabledMarkdownTransformers() {
+ const transformers = [ ...TRANSFORMERS, HORIZONTAL_DIVIDER ]
+ if (this.supportsCode) return transformers
+
+ return transformers.filter((transformer) => transformer !== CODE && transformer !== INLINE_CODE)
+ }
+
#registerTableComponents() {
let tableTools = this.querySelector("lexxy-table-tools")
tableTools ??= createElement("lexxy-table-tools")
@@ -607,6 +665,41 @@ export class LexicalEditorElement extends HTMLElement {
this.#disposables.push(codeLanguagePicker)
}
+ // The import conversions and the FORMAT_TEXT_COMMAND interceptor cover HTML and command
+ // paths, but content pasted from another Lexical editor arrives as serialized nodes whose
+ // format bitfields are restored directly. This transform clears any disabled-mark bit so a
+ // disabled mark can never render in the editor, whatever path produced it.
+ #registerDisabledMarkStripper(registered) {
+ const disabledMarks = this.disabledMarks
+ if (disabledMarks.length === 0) return
+
+ registered.push(this.editor.registerNodeTransform(TextNode, (node) => {
+ for (const mark of disabledMarks) {
+ if (node.hasFormat(mark)) node.toggleFormat(mark)
+ }
+ }))
+ }
+
+ // Highlight is stored as color/background-color styles (plus the highlight format bit)
+ // on text nodes. The import conversions strip and legacy Trix color, but content
+ // pasted from another Lexxy editor arrives as serialized nodes whose styles are restored
+ // directly — bypassing those conversions. 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))
+ }
+ }))
+ }
+
#handleEnter() {
// We can't prevent these externally using regular keydown because Lexical handles it first.
this.#listeners.track(this.editor.registerCommand(
@@ -716,6 +809,13 @@ 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.setAttribute("data-highlight", this.supportsHighlight) // Drives toolbar CSS styles
+ toolbar.setAttribute("data-disabled-marks", this.disabledMarks.join(" ")) // Drives toolbar CSS styles
+ if (!this.supportsCode) toolbar.querySelector("[name='code']")?.remove()
+ for (const level of HEADING_LEVELS) {
+ if (!this.#enabledHeadings.includes(level.tag)) toolbar.querySelector(`[name='${level.name}']`)?.remove()
+ }
toolbar.configure(this.config.get("toolbar"))
this.prepend(toolbar)
return toolbar
@@ -725,6 +825,33 @@ export class LexicalEditorElement extends HTMLElement {
this.classList.toggle("lexxy-editor--empty", this.isEmpty)
}
+ // Drop HTML import conversions for disabled features so their markup is reduced to plain
+ // text on every import path (initial value, setValue, paste) and excluded from the
+ // sanitizer allow-list, which #getImportableTags derives from these same conversion keys.
+ #removeDisabledConversions(editor) {
+ if (!this.supportsHighlight) {
+ // The highlight extension owns the import conversion, but Lexical's TextNode
+ // also imports as its built-in highlight format. When highlight is disabled the
+ // extension isn't registered, so drop the conversion entirely.
+ editor._htmlConversions?.delete("mark")
+ }
+
+ if (!this.supportsCode) {
+ // CodeNode is not registered when code is disabled, but the `code` (inline) and `pre`
+ // HTML conversions remain — TextNode.importDOM registers `code` independently of CodeNode.
+ editor._htmlConversions?.delete("code")
+ editor._htmlConversions?.delete("pre")
+ }
+
+ // Each disabled mark's tag keys hold both Lexical's default TextNode conversion and the
+ // legacy Trix conversion, so deleting them strips the mark regardless of which produced it.
+ for (const mark of this.disabledMarks) {
+ for (const tag of MARK_TO_TAGS[mark]) {
+ editor._htmlConversions?.delete(tag)
+ }
+ }
+ }
+
#configureSanitizer(editor) {
setSanitizerConfig(this.#getAllowedElements(editor))
}
@@ -755,11 +882,12 @@ export class LexicalEditorElement extends HTMLElement {
const linkNode = $getNearestNodeOfType(anchorNode, LinkNode)
attributes = {
- bold: { active: format.isBold, enabled: true },
- italic: { active: format.isItalic, enabled: true },
- strikethrough: { active: format.isStrikethrough, enabled: true },
+ bold: { active: format.isBold, enabled: this.enabledMarks.includes("bold") },
+ italic: { active: format.isItalic, enabled: this.enabledMarks.includes("italic") },
+ strikethrough: { active: format.isStrikethrough, enabled: this.enabledMarks.includes("strikethrough") },
+ underline: { active: format.isUnderline, enabled: this.enabledMarks.includes("underline") },
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 },
quote: { active: format.isInQuote, enabled: true },
heading: { active: format.isInHeading, enabled: true },
@@ -770,7 +898,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
})
@@ -804,6 +932,8 @@ export class LexicalEditorElement extends HTMLElement {
}
get #resolvedHighlightColors() {
+ if (!this.supportsHighlight) return null
+
const buttons = this.config.get("highlight.buttons")
if (!buttons) return null
@@ -812,14 +942,22 @@ export class LexicalEditorElement extends HTMLElement {
return { colors, backgroundColors }
}
+ // The heading levels the toolbar/format menu offers, driven by the `headings` config
+ // option. Markdown shortcuts (#…######) still produce headings regardless of this.
+ get #enabledHeadings() {
+ const configured = this.config.get("headings")
+ return Array.isArray(configured) ? configured : HEADING_LEVELS.map((level) => level.tag)
+ }
+
get #supportedHeadingFormats() {
if (!this.supportsRichText) return []
+ const enabled = this.#enabledHeadings
return [
{ label: "Normal", command: "setFormatParagraph", tag: null },
- { label: "Large heading", command: "setFormatHeadingLarge", tag: "h2" },
- { label: "Medium heading", command: "setFormatHeadingMedium", tag: "h3" },
- { label: "Small heading", command: "setFormatHeadingSmall", tag: "h4" },
+ ...HEADING_LEVELS
+ .filter((level) => enabled.includes(level.tag))
+ .map(({ label, command, tag }) => ({ label, command, tag })),
]
}
diff --git a/src/elements/toolbar.js b/src/elements/toolbar.js
index 366e3c360..bb95c9d2b 100644
--- a/src/elements/toolbar.js
+++ b/src/elements/toolbar.js
@@ -11,6 +11,7 @@ import { ListenerBin, registerEventListener } from "../helpers/listener_helper"
import { handleRollingTabIndex } from "../helpers/accessibility_helper"
import ToolbarIcons from "./toolbar_icons"
import { generateDomId, isActiveAndVisible } from "../helpers/html_helper"
+import { HEADING_LEVELS } from "../editor/headings"
export class LexicalToolbarElement extends HTMLElement {
static observedAttributes = [ "connected" ]
@@ -232,9 +233,9 @@ export class LexicalToolbarElement extends HTMLElement {
this.#setButtonPressed("format", isInHeading)
this.#setButtonPressed("paragraph", !isInHeading)
- this.#setButtonPressed("heading-large", headingTag === "h2")
- this.#setButtonPressed("heading-medium", headingTag === "h3")
- this.#setButtonPressed("heading-small", headingTag === "h4")
+ for (const level of HEADING_LEVELS) {
+ this.#setButtonPressed(level.name, headingTag === level.tag)
+ }
this.#setButtonPressed("lists", isInList)
this.#setButtonPressed("unordered-list", isInList && listType === "bullet")
@@ -413,6 +414,9 @@ export class LexicalToolbarElement extends HTMLElement {
+
diff --git a/src/elements/toolbar_icons.js b/src/elements/toolbar_icons.js
index 5867b13b9..fcd86362d 100644
--- a/src/elements/toolbar_icons.js
+++ b/src/elements/toolbar_icons.js
@@ -26,6 +26,11 @@ export default {
`,
+ "h1":
+ ``,
+
"h2":
`