Lexical: Added button icon system

With a bunch of default icons
This commit is contained in:
Dan Brown 2024-06-19 20:00:29 +01:00
parent e2409a5fab
commit 13d970c7ce
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
34 changed files with 99 additions and 16 deletions

View File

@ -32,6 +32,13 @@ esbuild.build({
format: 'esm',
minify: isProd,
logLevel: 'info',
loader: {
'.svg': 'text',
},
absWorkingDir: path.join(__dirname, '../..'),
alias: {
'@icons': './resources/icons',
},
banner: {
js: '// See the "/licenses" URI for full package license details',
css: '/* See the "/licenses" URI for full package license details */',

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm160-160v-80h400v80H280ZM120-440v-80h720v80H120Zm160-160v-80h400v80H280ZM120-760v-80h720v80H120Z"/></svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Zm0-160v-80h720v80H120Z"/></svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm0-160v-80h480v80H120Zm0-160v-80h720v80H120Zm0-160v-80h480v80H120Zm0-160v-80h720v80H120Z"/></svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-760v-80h720v80H120Zm240 160v-80h480v80H360ZM120-440v-80h720v80H120Zm240 160v-80h480v80H360ZM120-120v-80h720v80H120Z"/></svg>

After

Width:  |  Height:  |  Size: 203 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M272-200v-560h221q65 0 120 40t55 111q0 51-23 78.5T602-491q25 11 55.5 41t30.5 90q0 89-65 124.5T501-200H272Zm121-112h104q48 0 58.5-24.5T566-372q0-11-10.5-35.5T494-432H393v120Zm0-228h93q33 0 48-17t15-38q0-24-17-39t-44-15h-95v109Z"/></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m384-336 56-57-87-87 87-87-56-57-144 144 144 144Zm192 0 144-144-144-144-56 57 87 87-87 87 56 57ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z"/></svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-240 80-480l240-240 57 57-184 184 183 183-56 56Zm320 0-57-57 184-184-183-183 56-56 240 240-240 240Z"/></svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-480H200v480Zm80-280v-80h400v80H280Zm0 160v-80h240v80H280Z"/></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m528-546-93-93-121-121h486v120H568l-40 94ZM792-56 460-388l-80 188H249l119-280L56-792l56-56 736 736-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>

After

Width:  |  Height:  |  Size: 653 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M160-440v-80h640v80H160Z"/></svg>

After

Width:  |  Height:  |  Size: 107 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h360v80H200v560h560v-360h80v360q0 33-23.5 56.5T760-120H200Zm480-480v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM240-280h480L570-480 450-320l-90-120-120 160Zm-40-480v560-560Z"/></svg>

After

Width:  |  Height:  |  Size: 315 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm160 440L120-480l160-160v320Z"/></svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M120-120v-80h720v80H120Zm320-160v-80h400v80H440Zm0-160v-80h400v80H440Zm0-160v-80h400v80H440ZM120-760v-80h720v80H120Zm0 440v-320l160 160-160 160Z"/></svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-200v-100h160l120-360H320v-100h400v100H580L460-300h140v100H200Z"/></svg>

After

Width:  |  Height:  |  Size: 150 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M680-160v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm560-40h-80q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480Z"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M360-200v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360ZM200-160q-33 0-56.5-23.5T120-240q0-33 23.5-56.5T200-320q33 0 56.5 23.5T280-240q0 33-23.5 56.5T200-160Zm0-240q-33 0-56.5-23.5T120-480q0-33 23.5-56.5T200-560q33 0 56.5 23.5T280-480q0 33-23.5 56.5T200-400Zm0-240q-33 0-56.5-23.5T120-720q0-33 23.5-56.5T200-800q33 0 56.5 23.5T280-720q0 33-23.5 56.5T200-640Z"/></svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M222-200 80-342l56-56 85 85 170-170 56 57-225 226Zm0-320L80-662l56-56 85 85 170-170 56 57-225 226Zm298 240v-80h360v80H520Zm0-320v-80h360v80H520Z"/></svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M120-80v-60h100v-30h-60v-60h60v-30H120v-60h120q17 0 28.5 11.5T280-280v40q0 17-11.5 28.5T240-200q17 0 28.5 11.5T280-160v40q0 17-11.5 28.5T240-80H120Zm0-280v-110q0-17 11.5-28.5T160-510h60v-30H120v-60h120q17 0 28.5 11.5T280-560v70q0 17-11.5 28.5T240-450h-60v30h100v60H120Zm60-280v-180h-60v-60h120v240h-60Zm180 440v-80h480v80H360Zm0-240v-80h480v80H360Zm0-240v-80h480v80H360Z"/></svg>

After

Width:  |  Height:  |  Size: 468 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" ><path d="M396-200q-97 0-166.5-63T160-420q0-94 69.5-157T396-640h252L544-744l56-56 200 200-200 200-56-56 104-104H396q-63 0-109.5 40T240-420q0 60 46.5 100T396-280h284v80H396Z"/></svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m384-336 56-58-86-86 86-86-56-58-144 144 144 144Zm192 0 144-144-144-144-56 58 86 86-86 86 56 58ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h168q13-36 43.5-58t68.5-22q38 0 68.5 22t43.5 58h168q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm280-590q13 0 21.5-8.5T510-820q0-13-8.5-21.5T480-850q-13 0-21.5 8.5T450-820q0 13 8.5 21.5T480-790ZM200-200v-560 560Z"/></svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M80-400v-80h800v80H80Zm340-160v-120H200v-120h560v120H540v120H420Zm0 400v-160h120v160H420Z"/></svg>

After

Width:  |  Height:  |  Size: 172 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-160v-80q0-17 11.5-28.5T800-280h80v-40H760v-40h120q17 0 28.5 11.5T920-320v40q0 17-11.5 28.5T880-240h-80v40h120v40H760Zm-525-80 185-291-172-269h106l124 200h4l123-200h107L539-531l186 291H618L482-457h-4L342-240H235Z"/></svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M760-600v-80q0-17 11.5-28.5T800-720h80v-40H760v-40h120q17 0 28.5 11.5T920-760v40q0 17-11.5 28.5T880-680h-80v40h120v40H760ZM235-160l185-291-172-269h106l124 200h4l123-200h107L539-451l186 291H618L482-377h-4L342-160H235Z"/></svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M200-120v-80h560v80H200Zm280-160q-101 0-157-63t-56-167v-330h103v336q0 56 28 91t82 35q54 0 82-35t28-91v-336h103v330q0 104-56 167t-157 63Z"/></svg>

After

Width:  |  Height:  |  Size: 219 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z"/></svg>

After

Width:  |  Height:  |  Size: 244 B

4
resources/js/global.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.svg' {
const content: string;
export default content;
}

View File

@ -9,11 +9,13 @@ import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
import {$setBlocksType} from "@lexical/selection";
export function el(tag: string, attrs: Record<string, string> = {}, children: (string|HTMLElement)[] = []): HTMLElement {
export function el(tag: string, attrs: Record<string, string|null> = {}, children: (string|HTMLElement)[] = []): HTMLElement {
const el = document.createElement(tag);
const attrKeys = Object.keys(attrs);
for (const attr of attrKeys) {
el.setAttribute(attr, attrs[attr]);
if (attrs[attr] !== null) {
el.setAttribute(attr, attrs[attr] as string);
}
}
for (const child of children) {

View File

@ -29,9 +29,27 @@ import {$isImageNode, ImageNode} from "../../nodes/image";
import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
import {getEditorContentAsHtml} from "../../actions";
import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list";
import undoIcon from "@icons/editor/undo.svg"
import redoIcon from "@icons/editor/redo.svg"
import boldIcon from "@icons/editor/bold.svg"
import italicIcon from "@icons/editor/italic.svg"
import underlinedIcon from "@icons/editor/underlined.svg"
import strikethroughIcon from "@icons/editor/strikethrough.svg"
import superscriptIcon from "@icons/editor/superscript.svg"
import subscriptIcon from "@icons/editor/subscript.svg"
import codeIcon from "@icons/editor/code.svg"
import formatClearIcon from "@icons/editor/format-clear.svg"
import listBulletIcon from "@icons/editor/list-bullet.svg"
import listNumberedIcon from "@icons/editor/list-numbered.svg"
import listCheckIcon from "@icons/editor/list-check.svg"
import linkIcon from "@icons/editor/link.svg"
import imageIcon from "@icons/editor/image.svg"
import detailsIcon from "@icons/editor/details.svg"
import sourceIcon from "@icons/editor/source-view.svg"
export const undo: EditorButtonDefinition = {
label: 'Undo',
icon: undoIcon,
action(context: EditorUiContext) {
context.editor.dispatchCommand(UNDO_COMMAND, undefined);
},
@ -42,6 +60,7 @@ export const undo: EditorButtonDefinition = {
export const redo: EditorButtonDefinition = {
label: 'Redo',
icon: redoIcon,
action(context: EditorUiContext) {
context.editor.dispatchCommand(REDO_COMMAND, undefined);
},
@ -116,9 +135,10 @@ export const paragraph: EditorButtonDefinition = {
}
}
function buildFormatButton(label: string, format: TextFormatType): EditorButtonDefinition {
function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition {
return {
label: label,
icon,
action(context: EditorUiContext) {
context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
},
@ -128,18 +148,19 @@ function buildFormatButton(label: string, format: TextFormatType): EditorButtonD
};
}
export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold');
export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic');
export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline');
export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon);
export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon);
export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon);
export const textColor: EditorBasicButtonDefinition = {label: 'Text color'};
export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color'};
export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough');
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript');
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript');
export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code');
export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon);
export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon);
export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon);
export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon);
export const clearFormating: EditorButtonDefinition = {
label: 'Clear formatting',
icon: formatClearIcon,
action(context: EditorUiContext) {
context.editor.update(() => {
const selection = $getSelection();
@ -155,9 +176,10 @@ export const clearFormating: EditorButtonDefinition = {
}
};
function buildListButton(label: string, type: ListType): EditorButtonDefinition {
function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition {
return {
label,
icon,
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
const selection = $getSelection();
@ -176,13 +198,14 @@ function buildListButton(label: string, type: ListType): EditorButtonDefinition
};
}
export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet');
export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number');
export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check');
export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon);
export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon);
export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon);
export const link: EditorButtonDefinition = {
label: 'Insert/edit link',
icon: linkIcon,
action(context: EditorUiContext) {
const linkModal = context.manager.createModal('link');
context.editor.getEditorState().read(() => {
@ -215,6 +238,7 @@ export const link: EditorButtonDefinition = {
export const image: EditorButtonDefinition = {
label: 'Insert/Edit Image',
icon: imageIcon,
action(context: EditorUiContext) {
const imageModal = context.manager.createModal('image');
const selection = context.lastSelection;
@ -247,6 +271,7 @@ export const image: EditorButtonDefinition = {
export const details: EditorButtonDefinition = {
label: 'Insert collapsible block',
icon: detailsIcon,
action(context: EditorUiContext) {
context.editor.update(() => {
const selection = $getSelection();
@ -274,6 +299,7 @@ export const details: EditorButtonDefinition = {
export const source: EditorButtonDefinition = {
label: 'Source code',
icon: sourceIcon,
async action(context: EditorUiContext) {
const modal = context.manager.createModal('source');
const source = await getEditorContentAsHtml(context.editor);

View File

@ -4,6 +4,7 @@ import {el} from "../../helpers";
export interface EditorBasicButtonDefinition {
label: string;
icon?: string|undefined;
}
export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
@ -21,10 +22,19 @@ export class EditorButton extends EditorUiElement {
}
protected buildDOM(): HTMLButtonElement {
const label = this.getLabel();
let child: string|HTMLElement = label;
if (this.definition.icon) {
child = el('span', {class: 'editor-button-icon'});
child.innerHTML = this.definition.icon;
}
const button = el('button', {
type: 'button',
class: 'editor-button',
}, [this.getLabel()]) as HTMLButtonElement;
title: this.definition.icon ? label : null,
}, [child]) as HTMLButtonElement;
button.addEventListener('click', this.onClick.bind(this));

View File

@ -16,6 +16,7 @@ import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
import {EditorColorPicker} from "./framework/blocks/color-picker";
console.log(undo);
export function getMainEditorFullToolbar(): EditorContainerUiElement {
return new EditorSimpleClassContainer('editor-toolbar-main', [

View File

@ -29,6 +29,11 @@
padding: 4px 6px;
display: block;
}
.editor-button-icon svg {
width: 24px;
height: 24px;
fill: #000;
}
// Containers
.editor-dropdown-menu-container {

View File

@ -29,7 +29,9 @@
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@icons/*": ["./resources/icons/*"]
},
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */