Skip to content
Draft
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
15 changes: 15 additions & 0 deletions app/assets/stylesheets/lexxy-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<lexxy-editor marks='["bold", "italic"]'></lexxy-editor>`.
- `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.
- `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: `<lexxy-editor headings='["h1", "h2", "h3"]'></lexxy-editor>`.
Comment on lines 53 to +56

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">`.
5 changes: 5 additions & 0 deletions src/config/lexxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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})`),
Expand Down
28 changes: 28 additions & 0 deletions src/editor/command_dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
$isRangeSelection,
$isTextNode,
$setSelection,
COMMAND_PRIORITY_HIGH,
COMMAND_PRIORITY_NORMAL,
FORMAT_TEXT_COMMAND,
INDENT_CONTENT_COMMAND,
Expand Down Expand Up @@ -33,6 +34,7 @@ const COMMANDS = [
"unlink",
"toggleHighlight",
"removeHighlight",
"setFormatHeadingHuge",
"setFormatHeadingLarge",
"setFormatHeadingMedium",
"setFormatHeadingSmall",
Expand Down Expand Up @@ -68,6 +70,7 @@ export class CommandDispatcher {
this.contents = editorElement.contents

this.#registerCommands()
this.#registerDisabledMarkInterceptor()
this.#registerKeyboardCommands()
this.#registerDragAndDropHandlers()
}
Expand Down Expand Up @@ -156,6 +159,8 @@ export class CommandDispatcher {
}

dispatchInsertCodeBlock() {
if (!this.editorElement.supportsCode) return

if (this.selection.hasSelectedWordsInSingleLine) {
this.#toggleInlineCode()
} else {
Expand Down Expand Up @@ -228,6 +233,10 @@ export class CommandDispatcher {
$insertNodeToNearestRoot(new HorizontalDividerNode)
}

dispatchSetFormatHeadingHuge() {
this.contents.applyHeadingFormat("h1")
}

dispatchSetFormatHeadingLarge() {
this.contents.applyHeadingFormat("h2")
}
Expand Down Expand Up @@ -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 })
}

Expand All @@ -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))
}
Expand Down
11 changes: 11 additions & 0 deletions src/editor/headings.js
Original file line number Diff line number Diff line change
@@ -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" },
]
32 changes: 32 additions & 0 deletions src/editor/marks.js
Original file line number Diff line number Diff line change
@@ -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. `<em style="color:…">`), 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))
)
}
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
Loading