Skip to content

Commit 8d58356

Browse files
Merge pull request #379 from basecamp/undo-redo
Undo/redo
2 parents 7b4a0cb + 97ce54f commit 8d58356

File tree

12 files changed

+157
-15
lines changed

12 files changed

+157
-15
lines changed

app/assets/javascript/lexxy.js

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5125,6 +5125,8 @@ class LexicalToolbarElement extends HTMLElement {
51255125
this.#bindHotkeys();
51265126
this.#assignButtonTabindex();
51275127
this.#monitorSelectionChanges();
5128+
this.#monitorHistoryChanges();
5129+
this.#refreshToolbarOverflow();
51285130
}
51295131

51305132
#bindButtons() {
@@ -5200,6 +5202,30 @@ class LexicalToolbarElement extends HTMLElement {
52005202
});
52015203
}
52025204

5205+
#monitorHistoryChanges() {
5206+
this.editor.registerUpdateListener(() => {
5207+
this.#updateUndoRedoButtonStates();
5208+
});
5209+
}
5210+
5211+
#updateUndoRedoButtonStates() {
5212+
this.editor.getEditorState().read(() => {
5213+
const historyState = this.editorElement.historyState;
5214+
if (historyState) {
5215+
this.#setButtonDisabled("undo", historyState.undoStack.length === 0);
5216+
this.#setButtonDisabled("redo", historyState.redoStack.length === 0);
5217+
}
5218+
});
5219+
}
5220+
5221+
#setButtonDisabled(name, isDisabled) {
5222+
const button = this.querySelector(`[name="${name}"]`);
5223+
if (button) {
5224+
button.disabled = isDisabled;
5225+
button.setAttribute("aria-disabled", isDisabled.toString());
5226+
}
5227+
}
5228+
52035229
#updateButtonStates() {
52045230
const selection = Nr();
52055231
if (!cr(selection)) return
@@ -5228,6 +5254,8 @@ class LexicalToolbarElement extends HTMLElement {
52285254
this.#setButtonPressed("quote", isInQuote);
52295255
this.#setButtonPressed("heading", isInHeading);
52305256
this.#setButtonPressed("link", isInLink);
5257+
5258+
this.#updateUndoRedoButtonStates();
52315259
}
52325260

52335261
#isSelectionInInlineCode(selection) {
@@ -5300,17 +5328,17 @@ class LexicalToolbarElement extends HTMLElement {
53005328

53015329
for (const button of buttons) {
53025330
if (this.#toolbarIsOverflowing()) {
5303-
this.#overflowMenu.appendChild(button);
5331+
this.#overflowMenu.prepend(button);
53045332
movedToOverflow = true;
53055333
} else {
5306-
if (movedToOverflow) this.#overflowMenu.appendChild(button);
5334+
if (movedToOverflow) this.#overflowMenu.prepend(button);
53075335
break
53085336
}
53095337
}
53105338
}
53115339

53125340
get #buttons() {
5313-
return Array.from(this.querySelectorAll(":scope > button"))
5341+
return Array.from(this.querySelectorAll(":scope > button, :scope > [role=separator]"))
53145342
}
53155343

53165344
static get defaultTemplate() {
@@ -5372,6 +5400,16 @@ class LexicalToolbarElement extends HTMLElement {
53725400
<button class="lexxy-editor__toolbar-button" type="button" name="divider" data-command="insertHorizontalDivider" title="Insert a divider">
53735401
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M0 12C0 11.4477 0.447715 11 1 11H23C23.5523 11 24 11.4477 24 12C24 12.5523 23.5523 13 23 13H1C0.447716 13 0 12.5523 0 12Z"/><path d="M4 5C4 3.89543 4.89543 3 6 3H18C19.1046 3 20 3.89543 20 5C20 6.10457 19.1046 7 18 7H6C4.89543 7 4 6.10457 4 5Z"/><path d="M4 19C4 17.8954 4.89543 17 6 17H18C19.1046 17 20 17.8954 20 19C20 20.1046 19.1046 21 18 21H6C4.89543 21 4 20.1046 4 19Z"/></svg>
53745402
</button>
5403+
5404+
<div class="lexxy-editor__toolbar-spacer" role="separator"></div>
5405+
5406+
<button class="lexxy-editor__toolbar-button" type="button" name="undo" data-command="undo" title="Undo" data-hotkey="cmd+z ctrl+z">
5407+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M5.64648 8.26531C7.93911 6.56386 10.7827 5.77629 13.624 6.05535C16.4655 6.33452 19.1018 7.66079 21.0195 9.77605C22.5839 11.5016 23.5799 13.6516 23.8936 15.9352C24.0115 16.7939 23.2974 17.4997 22.4307 17.4997C21.5641 17.4997 20.8766 16.7915 20.7148 15.9401C20.4295 14.4379 19.7348 13.0321 18.6943 11.8844C17.3 10.3464 15.3835 9.38139 13.3174 9.17839C11.2514 8.97546 9.18359 9.54856 7.5166 10.7858C6.38259 11.6275 5.48981 12.7361 4.90723 13.9997H8.5C9.3283 13.9997 9.99979 14.6714 10 15.4997C10 16.3281 9.32843 16.9997 8.5 16.9997H1.5C0.671573 16.9997 0 16.3281 0 15.4997V8.49968C0.000213656 7.67144 0.671705 6.99968 1.5 6.99968C2.3283 6.99968 2.99979 7.67144 3 8.49968V11.0212C3.7166 9.9704 4.60793 9.03613 5.64648 8.26531Z"/></svg>
5408+
</button>
5409+
5410+
<button class="lexxy-editor__toolbar-button" type="button" name="redo" data-command="redo" title="Redo" data-hotkey="cmd+shift+z ctrl+shift+z ctrl+y">
5411+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M18.2599 8.26531C15.9672 6.56386 13.1237 5.77629 10.2823 6.05535C7.4408 6.33452 4.80455 7.66079 2.88681 9.77605C1.32245 11.5016 0.326407 13.6516 0.0127834 15.9352C-0.105117 16.7939 0.608975 17.4997 1.47567 17.4997C2.34228 17.4997 3.02969 16.7915 3.19149 15.9401C3.47682 14.4379 4.17156 13.0321 5.212 11.8844C6.60637 10.3464 8.52287 9.38139 10.589 9.17839C12.655 8.97546 14.7227 9.54856 16.3897 10.7858C17.5237 11.6275 18.4165 12.7361 18.9991 13.9997H15.4063C14.578 13.9997 13.9066 14.6714 13.9063 15.4997C13.9063 16.3281 14.5779 16.9997 15.4063 16.9997H22.4063C23.2348 16.9997 23.9063 16.3281 23.9063 15.4997V8.49968C23.9061 7.67144 23.2346 6.99968 22.4063 6.99968C21.578 6.99968 20.9066 7.67144 20.9063 8.49968V11.0212C20.1897 9.9704 19.2984 9.03613 18.2599 8.26531Z"/></svg>
5412+
</button>
53755413

53765414
<details class="lexxy-editor__toolbar-overflow">
53775415
<summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
@@ -6004,7 +6042,9 @@ const COMMANDS = [
60046042
"insertQuoteBlock",
60056043
"insertCodeBlock",
60066044
"insertHorizontalDivider",
6007-
"uploadAttachments"
6045+
"uploadAttachments",
6046+
"undo",
6047+
"redo"
60086048
];
60096049

60106050
class CommandDispatcher {
@@ -6140,6 +6180,14 @@ class CommandDispatcher {
61406180
setTimeout(() => input.remove(), 1000);
61416181
}
61426182

6183+
dispatchUndo() {
6184+
this.editor.dispatchCommand(xe$1, undefined);
6185+
}
6186+
6187+
dispatchRedo() {
6188+
this.editor.dispatchCommand(Ce$1, undefined);
6189+
}
6190+
61436191
#registerCommands() {
61446192
for (const command of COMMANDS) {
61456193
const methodName = `dispatch${capitalize(command)}`;
@@ -7986,7 +8034,8 @@ class LexicalEditorElement extends HTMLElement {
79868034

79878035
#registerComponents() {
79888036
Mt(this.editor);
7989-
v$1(this.editor, E$1(), 20);
8037+
this.historyState = E$1();
8038+
v$1(this.editor, this.historyState, 20);
79908039
_t$3(this.editor);
79918040
this.#registerCodeHiglightingComponents();
79928041
et(this.editor, Bt);

app/assets/javascript/lexxy.js.br

709 Bytes
Binary file not shown.

app/assets/javascript/lexxy.js.gz

121 Bytes
Binary file not shown.

app/assets/javascript/lexxy.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
744 Bytes
Binary file not shown.
335 Bytes
Binary file not shown.

app/assets/stylesheets/lexxy-editor.css

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
inline-size: auto;
3737

3838
@media(any-hover: hover) {
39-
&:hover {
39+
&:hover:not([aria-disabled="true"]) {
4040
background: var(--lexxy-color-ink-lightest);
4141
}
4242
}
@@ -103,7 +103,7 @@
103103
display: grid;
104104
place-items: center;
105105

106-
&:is(:active),
106+
&:is(:active):not([aria-disabled="true"]),
107107
&[aria-pressed="true"] {
108108
background-color: var(--lexxy-color-selected);
109109

@@ -112,6 +112,11 @@
112112
}
113113
}
114114

115+
&[aria-disabled="true"] {
116+
cursor: default;
117+
opacity: 0.3;
118+
}
119+
115120
svg {
116121
-webkit-touch-callout: none;
117122
block-size: var(--lexxy-toolbar-icon-size);
@@ -122,8 +127,18 @@
122127
}
123128
}
124129

130+
:where(.lexxy-editor__toolbar-spacer) {
131+
flex: 1;
132+
}
133+
134+
/* Make sure spacer is only displayed if there's another button before it */
135+
* + :where(.lexxy-editor__toolbar-spacer) {
136+
min-inline-size: 1lh;
137+
}
138+
125139
:where(.lexxy-editor__toolbar-overflow) {
126140
display: none;
141+
justify-self: flex-end;
127142
position: relative;
128143
z-index: 1;
129144

docs/images/home.screenshot.png

-17.5 KB
Loading

src/editor/command_dispatcher.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
$isRangeSelection,
44
PASTE_COMMAND,
55
COMMAND_PRIORITY_LOW,
6-
FORMAT_TEXT_COMMAND
6+
FORMAT_TEXT_COMMAND,
7+
UNDO_COMMAND,
8+
REDO_COMMAND
79
} from "lexical"
810

911
import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list"
@@ -26,7 +28,9 @@ const COMMANDS = [
2628
"insertQuoteBlock",
2729
"insertCodeBlock",
2830
"insertHorizontalDivider",
29-
"uploadAttachments"
31+
"uploadAttachments",
32+
"undo",
33+
"redo"
3034
]
3135

3236
export class CommandDispatcher {
@@ -162,6 +166,14 @@ export class CommandDispatcher {
162166
setTimeout(() => input.remove(), 1000)
163167
}
164168

169+
dispatchUndo() {
170+
this.editor.dispatchCommand(UNDO_COMMAND, undefined)
171+
}
172+
173+
dispatchRedo() {
174+
this.editor.dispatchCommand(REDO_COMMAND, undefined)
175+
}
176+
165177
#registerCommands() {
166178
for (const command of COMMANDS) {
167179
const methodName = `dispatch${capitalize(command)}`

src/elements/editor.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ export default class LexicalEditorElement extends HTMLElement {
279279

280280
#registerComponents() {
281281
registerRichText(this.editor)
282-
registerHistory(this.editor, createEmptyHistoryState(), 20)
282+
this.historyState = createEmptyHistoryState()
283+
registerHistory(this.editor, this.historyState, 20)
283284
registerList(this.editor)
284285
this.#registerCodeHiglightingComponents()
285286
registerMarkdownShortcuts(this.editor, TRANSFORMERS)

0 commit comments

Comments
 (0)