Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
36 changes: 24 additions & 12 deletions app/assets/javascript/lexxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4652,6 +4652,7 @@ const presets = new Configuration({
multiLine: true,
richText: true,
toolbar: true,
headings: [ "h2", "h3", "h4" ],
highlight: {
buttons: {
color: range(1, 9).map(n => `var(--highlight-${n})`),
Expand Down Expand Up @@ -5182,10 +5183,10 @@ class LexicalToolbarElement extends HTMLElement {
}

#closeDropdowns() {
this.#dropdowns.forEach((details) => {
details.open = false;
});
}
this.#dropdowns.forEach((details) => {
details.open = false;
});
}

get #dropdowns() {
return this.querySelectorAll("details")
Expand Down Expand Up @@ -8042,27 +8043,35 @@ class CommandDispatcher {
this.editor.focus();
}

get #configuredHeadings() {
const configured = this.editorElement.config.get("headings");
const headings = Array.isArray(configured) ? configured : [ "h2", "h3", "h4" ];
return headings.filter((heading) => /^h[1-6]$/.test(heading))
}

dispatchRotateHeadingFormat() {
const selection = Lr();
if (!yr(selection)) return

const headings = this.#configuredHeadings;
if (headings.length === 0) return

if (as(selection.anchor.getNode())) {
selection.insertNodes([ St$3("h2") ]);
selection.insertNodes([ St$3(headings[0]) ]);
return
}

const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow();
let nextTag = "h2";
let nextTag = headings[0];
if (It$2(topLevelElement)) {
const currentTag = topLevelElement.getTag();
if (currentTag === "h2") {
nextTag = "h3";
} else if (currentTag === "h3") {
nextTag = "h4";
} else if (currentTag === "h4") {
const currentIndex = headings.indexOf(currentTag);
if (currentIndex >= 0 && currentIndex < headings.length - 1) {
nextTag = headings[currentIndex + 1];
} else if (currentIndex === headings.length - 1) {
nextTag = null;
} else {
nextTag = "h2";
nextTag = headings[0];
}
}

Expand Down Expand Up @@ -8348,6 +8357,9 @@ class Selection {
isInLink: wt$5(anchorNode, y$2) !== null,
isInQuote: Ot$2(topLevelElement),
isInHeading: It$2(topLevelElement),
headingTag: It$2(topLevelElement)
? topLevelElement.getTag()
: null,
isInCode: selection.hasFormat("code") || wt$5(anchorNode, q$1) !== null,
isInList: listType !== null,
listType,
Expand Down
Binary file modified app/assets/javascript/lexxy.js.br
Binary file not shown.
Binary file modified app/assets/javascript/lexxy.js.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion app/assets/javascript/lexxy.min.js

Large diffs are not rendered by default.

Binary file modified app/assets/javascript/lexxy.min.js.br
Binary file not shown.
Binary file modified app/assets/javascript/lexxy.min.js.gz
Binary file not shown.
24 changes: 12 additions & 12 deletions app/assets/stylesheets/lexxy-editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
outline-offset: var(--lexxy-focus-ring-offset);
}
}

summary {
list-style: none;

Expand Down Expand Up @@ -145,7 +145,7 @@
}
&[data-action="toggle"] {
background-color: var(--lexxy-color-table-cell-toggle);

&:after { box-shadow: 0 0 0 0 transparent; }
}
}
Expand Down Expand Up @@ -235,7 +235,7 @@
&[data-attachments="false"] button[name="upload"]{
display: none;
}

.lexxy-editor__toolbar-button {
aspect-ratio: 1;
block-size: var(--lexxy-toolbar-button-size);
Expand Down Expand Up @@ -323,7 +323,7 @@
background-color: var(--lexxy-color-selected-hover);
}
}

[overflowing] &:not(.lexxy-editor__toolbar-overflow) summary ~ * {
inset-inline-end: var(--lexxy-toolbar-spacing);
inset-inline-start: var(--lexxy-toolbar-spacing);
Expand All @@ -344,7 +344,7 @@
display: none;
justify-self: flex-end;
z-index: 1;

summary ~ * {
border-start-end-radius: 0;
display: grid;
Expand Down Expand Up @@ -462,7 +462,7 @@
&[aria-pressed="true"] {
background-color: transparent;
box-shadow: 0 0 0 2px currentColor inset;

&:after {
content: "✓";
}
Expand All @@ -477,7 +477,7 @@
display: none;
}
}

[overflowing] & {
.lexxy-highlight-colors {
button {
Expand Down Expand Up @@ -566,7 +566,7 @@
.lexxy-table-control__more-menu {
gap: 0;
position: relative;

.lexxy-table-control__more-menu-details {
background: var(--lexxy-color-ink);
border-radius: var(--table-tools-radius);
Expand All @@ -578,7 +578,7 @@
padding: 0.25ch;
position: absolute;
transform: translateX(-50%);

button {
aspect-ratio: unset;
flex-direction: row;
Expand All @@ -588,11 +588,11 @@
padding: 0.75ch;
padding-inline-end: 1.5ch;
white-space: nowrap;

span {
display: inline-block;
}

svg {
block-size: 1.3lh;
inline-size: 1.3lh;
Expand All @@ -601,7 +601,7 @@
}
}
}

.lexxy-table-control__button--delete-table {
align-items: center;
aspect-ratio: unset;
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,
richText: true,
toolbar: true,
headings: [ "h2", "h3", "h4" ],
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

PR description says the defaults were changed to H1–H6, but the code sets the default preset headings to ["h2", "h3", "h4"]. Either update the description to match the implementation, or adjust the default headings preset here (and any other defaults) to reflect the intended behavior.

Copilot uses AI. Check for mistakes.
highlight: {
buttons: {
color: range(1, 9).map(n => `var(--highlight-${n})`),
Expand Down
24 changes: 16 additions & 8 deletions src/editor/command_dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,27 +152,35 @@ export class CommandDispatcher {
this.editor.focus()
}

get #configuredHeadings() {
const configured = this.editorElement.config.get("headings")
const headings = Array.isArray(configured) ? configured : [ "h2", "h3", "h4" ]
return headings.filter((heading) => /^h[1-6]$/.test(heading))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I feel like this is duplcating the default value. Mis-configuration producing errors is ok I think.

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.

Yeah - this is the one aspect having automated code review kind of misses and adds junk. Has you defensively programming around ghost scenarios that will likely never happen. Thanks for your thinking here

}

dispatchRotateHeadingFormat() {
const selection = $getSelection()
if (!$isRangeSelection(selection)) return

const headings = this.#configuredHeadings
if (headings.length === 0) return
Comment thread
ShugKnight24 marked this conversation as resolved.
Outdated

if ($isRootOrShadowRoot(selection.anchor.getNode())) {
selection.insertNodes([ $createHeadingNode("h2") ])
selection.insertNodes([ $createHeadingNode(headings[0]) ])
return
}

const topLevelElement = selection.anchor.getNode().getTopLevelElementOrThrow()
Comment thread
ShugKnight24 marked this conversation as resolved.
Outdated
let nextTag = "h2"
let nextTag = headings[0]
if ($isHeadingNode(topLevelElement)) {
const currentTag = topLevelElement.getTag()
if (currentTag === "h2") {
nextTag = "h3"
} else if (currentTag === "h3") {
nextTag = "h4"
} else if (currentTag === "h4") {
const currentIndex = headings.indexOf(currentTag)
if (currentIndex >= 0 && currentIndex < headings.length - 1) {
nextTag = headings[currentIndex + 1]
} else if (currentIndex === headings.length - 1) {
nextTag = null
} else {
nextTag = "h2"
nextTag = headings[0]
}
Comment thread
ShugKnight24 marked this conversation as resolved.
Outdated
}

Expand Down
3 changes: 3 additions & 0 deletions src/editor/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ export default class Selection {
isInLink: $getNearestNodeOfType(anchorNode, LinkNode) !== null,
isInQuote: $isQuoteNode(topLevelElement),
isInHeading: $isHeadingNode(topLevelElement),
headingTag: $isHeadingNode(topLevelElement)
? topLevelElement.getTag()
: null,
Comment thread
ShugKnight24 marked this conversation as resolved.
Outdated
isInCode: selection.hasFormat("code") || $getNearestNodeOfType(anchorNode, CodeNode) !== null,
isInList: listType !== null,
listType,
Expand Down
8 changes: 4 additions & 4 deletions src/elements/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,10 @@ export class LexicalToolbarElement extends HTMLElement {
}

#closeDropdowns() {
this.#dropdowns.forEach((details) => {
details.open = false
})
}
this.#dropdowns.forEach((details) => {
details.open = false
})
}
Comment on lines +309 to +312
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.

this indentation change looks correct


get #dropdowns() {
return this.querySelectorAll("details")
Expand Down
63 changes: 63 additions & 0 deletions test/javascript/editor/headings_configuration.test.js
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I ❤️ the use of presets here

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.

🙏🏻 - Thank copilot. I originally had each in the various tests and it pointed out I could have a preset based on the codebase

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect, test } from "vitest"
import { createElement } from "../helpers/dom_helper"
import EditorConfiguration from "../../../src/editor/configuration"
import { configure } from "../../../src/index"

configure({
default: {
headings: ["h2", "h3", "h4"]
},
blog: {
headings: ["h2", "h3", "h4"],
},
Comment thread
ShugKnight24 marked this conversation as resolved.
Outdated
minimal: {
headings: ["h2"],
},
noHeadings: {
headings: [],
},
})

test("uses default headings", () => {
const element = createElement("<lexxy-editor></lexxy-editor>")
const config = new EditorConfiguration(element)
expect(config.get("headings")).toEqual(["h2", "h3", "h4"])
})

test("overrides headings with attribute", () => {
const element = createElement(
'<lexxy-editor headings=\'["h1", "h2", "h3", "h4", "h5", "h6"]\'></lexxy-editor>'
)
const config = new EditorConfiguration(element)
expect(config.get("headings")).toEqual(["h1", "h2", "h3", "h4", "h5", "h6"])
})

test("overrides headings with attribute to include h1 and h5", () => {
const element = createElement(
'<lexxy-editor headings=\'["h1", "h2", "h5"]\'></lexxy-editor>'
)
const config = new EditorConfiguration(element)
expect(config.get("headings")).toEqual(["h1", "h2", "h5"])
})

test("overrides headings with preset", () => {
const element = createElement("<lexxy-editor preset='blog'></lexxy-editor>")
const config = new EditorConfiguration(element)
expect(config.get("headings")).toEqual(["h2", "h3", "h4"])
})

test("restricts headings to a subset", () => {
const element = createElement(
"<lexxy-editor preset='minimal'></lexxy-editor>"
)
const config = new EditorConfiguration(element)
expect(config.get("headings")).toEqual(["h2"])
})

test("handles empty headings array", () => {
const element = createElement(
"<lexxy-editor preset='noHeadings'></lexxy-editor>"
)
const config = new EditorConfiguration(element)
expect(config.get("headings")).toEqual([])
})