chore(lexical): start working on a toolbar

This commit is contained in:
Elian Doran
2026-03-20 18:11:27 +02:00
parent 3d860ba2c4
commit 715b87fb3d
3 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
.note-detail-editable-text {
.toolbar {
display: flex;
margin-bottom: 1px;
background: #fff;
padding: 4px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
vertical-align: middle;
}
.toolbar button.toolbar-item {
border: 0;
display: flex;
background: none;
border-radius: 10px;
padding: 8px;
cursor: pointer;
vertical-align: middle;
}
.toolbar button.toolbar-item:disabled {
cursor: not-allowed;
}
.toolbar button.toolbar-item.spaced {
margin-right: 2px;
}
.toolbar button.toolbar-item i.format {
background-size: contain;
display: inline-block;
height: 18px;
width: 18px;
margin-top: 2px;
vertical-align: -0.25em;
display: flex;
opacity: 0.6;
}
.toolbar button.toolbar-item:disabled i.format {
opacity: 0.2;
}
.toolbar button.toolbar-item.active {
background-color: rgba(223, 232, 250, 0.3);
}
.toolbar button.toolbar-item.active i {
opacity: 1;
}
.toolbar .toolbar-item:hover:not([disabled]) {
background-color: #eee;
}
.toolbar .divider {
width: 1px;
background-color: #eee;
margin: 0 4px;
}
.toolbar .toolbar-item .text {
display: flex;
line-height: 20px;
width: 200px;
vertical-align: middle;
font-size: 14px;
color: #777;
text-overflow: ellipsis;
width: 70px;
overflow: hidden;
height: 20px;
text-align: left;
}
.toolbar .toolbar-item .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
}
}

View File

@@ -0,0 +1,176 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import "./ToolbarPlugin.css";
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_LOW,
FORMAT_ELEMENT_COMMAND,
FORMAT_TEXT_COMMAND,
REDO_COMMAND,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useRef, useState} from 'react';
function Divider() {
return <div className="divider" />;
}
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const $updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
}
}, []);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(
() => {
$updateToolbar();
},
{editor},
);
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, _newEditor) => {
$updateToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, $updateToolbar]);
return (
<div className="toolbar" ref={toolbarRef}>
<button
disabled={!canUndo}
onClick={() => {
editor.dispatchCommand(UNDO_COMMAND, undefined);
}}
className="toolbar-item spaced"
aria-label="Undo">
<i className="format undo" />
</button>
<button
disabled={!canRedo}
onClick={() => {
editor.dispatchCommand(REDO_COMMAND, undefined);
}}
className="toolbar-item"
aria-label="Redo">
<i className="format redo" />
</button>
<Divider />
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
}}
className={`toolbar-item spaced ${ isBold ? 'active' : ''}`}
aria-label="Format Bold">
<i className="format bold" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}}
className={`toolbar-item spaced ${ isItalic ? 'active' : ''}`}
aria-label="Format Italics">
<i className="format italic" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}}
className={`toolbar-item spaced ${ isUnderline ? 'active' : ''}`}
aria-label="Format Underline">
<i className="format underline" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
}}
className={`toolbar-item spaced ${ isStrikethrough ? 'active' : ''}`}
aria-label="Format Strikethrough">
<i className="format strikethrough" />
</button>
<Divider />
<button
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'left');
}}
className="toolbar-item spaced"
aria-label="Left Align">
<i className="format left-align" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'center');
}}
className="toolbar-item spaced"
aria-label="Center Align">
<i className="format center-align" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'right');
}}
className="toolbar-item spaced"
aria-label="Right Align">
<i className="format right-align" />
</button>
<button
onClick={() => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, 'justify');
}}
className="toolbar-item"
aria-label="Justify Align">
<i className="format justify-align" />
</button>{' '}
</div>
);
}

View File

@@ -12,6 +12,7 @@ import { useEffect } from 'preact/hooks';
import { useEditorSpacedUpdate, useTriliumEvent } from '../../../react/hooks';
import { TypeWidgetProps } from "../../type_widget";
import ToolbarPlugin from "./ToolbarPlugin";
const theme = {
// Theme styling goes here
@@ -40,6 +41,7 @@ export default function LexicalText(props: TypeWidgetProps) {
return (
<LexicalComposer initialConfig={initialConfig}>
<ToolbarPlugin />
<div className="lexical-wrapper">
<RichTextPlugin
contentEditable={<ContentEditable /> as never}