Lexical: Added format previews to format buttons

This commit is contained in:
Dan Brown 2024-05-30 12:25:25 +01:00
parent dc1a40ea74
commit 57259aee00
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 185 additions and 39 deletions

View File

@ -19,22 +19,70 @@ export class EditorButton extends EditorUiElement {
protected buildDOM(): HTMLButtonElement {
const button = el('button', {
type: 'button',
class: 'editor-toolbar-button',
class: 'editor-button',
}, [this.definition.label]) as HTMLButtonElement;
button.addEventListener('click', event => {
this.definition.action(this.getContext().editor);
});
button.addEventListener('click', this.onClick.bind(this));
return button;
}
protected onClick() {
this.definition.action(this.getContext().editor);
}
updateActiveState(selection: BaseSelection|null) {
const isActive = this.definition.isActive(selection);
this.dom?.classList.toggle('editor-toolbar-button-active', isActive);
this.dom?.classList.toggle('editor-button-active', isActive);
}
updateState(state: EditorUiStateUpdate): void {
this.updateActiveState(state.selection);
}
}
export class FormatPreviewButton extends EditorButton {
protected previewSampleElement: HTMLElement;
constructor(previewSampleElement: HTMLElement,definition: EditorButtonDefinition) {
super(definition);
this.previewSampleElement = previewSampleElement;
}
protected buildDOM(): HTMLButtonElement {
const button = super.buildDOM();
button.innerHTML = '';
const preview = el('span', {
class: 'editor-button-format-preview'
}, [this.definition.label]);
const stylesToApply = this.getStylesFromPreview();
console.log(stylesToApply);
for (const style of Object.keys(stylesToApply)) {
preview.style.setProperty(style, stylesToApply[style]);
}
button.append(preview);
return button;
}
protected getStylesFromPreview(): Record<string, string> {
const wrap = el('div', {style: 'display: none', hidden: 'true', class: 'page-content'});
const sampleClone = this.previewSampleElement.cloneNode() as HTMLElement;
sampleClone.textContent = this.definition.label;
wrap.append(sampleClone);
document.body.append(wrap);
const propertiesToFetch = ['color', 'font-size', 'background-color', 'border-inline-start'];
const propertiesToReturn: Record<string, string> = {};
const computed = window.getComputedStyle(sampleClone);
for (const property of propertiesToFetch) {
propertiesToReturn[property] = computed.getPropertyValue(property);
}
wrap.remove();
return propertiesToReturn;
}
}

View File

@ -30,11 +30,62 @@ export class EditorContainerUiElement extends EditorUiElement {
}
}
export class EditorFormatMenu extends EditorContainerUiElement {
buildDOM(): HTMLElement {
return el('div', {
class: 'editor-format-menu'
}, this.getChildren().map(child => child.getDOMElement()));
export class EditorSimpleClassContainer extends EditorContainerUiElement {
protected className;
constructor(className: string, children: EditorUiElement[]) {
super(children);
this.className = className;
}
protected buildDOM(): HTMLElement {
return el('div', {
class: this.className,
}, this.getChildren().map(child => child.getDOMElement()));
}
}
export class EditorFormatMenu extends EditorContainerUiElement {
buildDOM(): HTMLElement {
const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
const menu = el('div', {
class: 'editor-format-menu-dropdown editor-dropdown-menu editor-menu-list',
hidden: 'true',
}, childElements);
const toggle = el('button', {
class: 'editor-format-menu-toggle',
type: 'button',
}, ['Formats']);
const wrapper = el('div', {
class: 'editor-format-menu editor-dropdown-menu-container',
}, [toggle, menu]);
let clickListener: Function|null = null;
const hide = () => {
menu.hidden = true;
if (clickListener) {
window.removeEventListener('click', clickListener as EventListener);
}
};
const show = () => {
menu.hidden = false
clickListener = (event: MouseEvent) => {
if (!wrapper.contains(event.target as HTMLElement)) {
hide();
}
}
window.addEventListener('click', clickListener as EventListener);
};
toggle.addEventListener('click', event => {
menu.hasAttribute('hidden') ? show() : hide();
});
menu.addEventListener('mouseleave', hide);
return wrapper;
}
}

View File

@ -1,4 +1,4 @@
import {EditorButton} from "./framework/buttons";
import {EditorButton, FormatPreviewButton} from "./framework/buttons";
import {
blockquote, bold, code,
dangerCallout,
@ -9,25 +9,26 @@ import {
undo,
warningCallout
} from "./defaults/button-definitions";
import {EditorContainerUiElement, EditorFormatMenu} from "./framework/containers";
import {EditorContainerUiElement, EditorFormatMenu, EditorSimpleClassContainer} from "./framework/containers";
import {el} from "../helpers";
export function getMainEditorFullToolbar(): EditorContainerUiElement {
return new EditorContainerUiElement([
return new EditorSimpleClassContainer('editor-toolbar-main', [
new EditorButton(undo),
new EditorButton(redo),
new EditorFormatMenu([
new EditorButton(h2),
new EditorButton(h3),
new EditorButton(h4),
new EditorButton(h5),
new EditorButton(blockquote),
new EditorButton(paragraph),
new EditorButton(infoCallout),
new EditorButton(successCallout),
new EditorButton(warningCallout),
new EditorButton(dangerCallout),
new FormatPreviewButton(el('h2'), h2),
new FormatPreviewButton(el('h3'), h3),
new FormatPreviewButton(el('h4'), h4),
new FormatPreviewButton(el('h5'), h5),
new FormatPreviewButton(el('blockquote'), blockquote),
new FormatPreviewButton(el('p'), paragraph),
new FormatPreviewButton(el('p', {class: 'callout info'}), infoCallout),
new FormatPreviewButton(el('p', {class: 'callout success'}), successCallout),
new FormatPreviewButton(el('p', {class: 'callout warning'}), warningCallout),
new FormatPreviewButton(el('p', {class: 'callout danger'}), dangerCallout),
]),
new EditorButton(bold),

View File

@ -0,0 +1,49 @@
// Main UI elements
.editor-toolbar-main {
display: flex;
}
// Buttons
.editor-button {
border: 1px solid #DDD;
font-size: 12px;
padding: 4px 6px;
color: #444;
}
.editor-button:hover {
background-color: #EEE;
cursor: pointer;
color: #000;
}
.editor-button-active, .editor-button-active:hover {
background-color: #ceebff;
color: #000;
}
.editor-button-format-preview {
padding: 4px 6px;
display: block;
}
// Containers
.editor-dropdown-menu-container {
position: relative;
}
.editor-dropdown-menu {
position: absolute;
background-color: #FFF;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.15);
z-index: 99;
min-width: 120px;
}
.editor-menu-list {
display: flex;
flex-direction: column;
}
.editor-menu-list > .editor-button {
border-bottom: 0;
text-align: start;
}
.editor-format-menu .editor-dropdown-menu {
min-width: 320px;
}

View File

@ -15,6 +15,7 @@
@import "forms";
@import "animations";
@import "tinymce";
@import "editor";
@import "codemirror";
@import "components";
@import "header";

View File

@ -6,23 +6,19 @@
option:wysiwyg-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
class="">
<style>
.editor-toolbar-button-active {
background-color: tomato;
}
</style>
<div class="editor-container">
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
<p id="Content!">Some <strong>content</strong> here</p>
<p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
<h2>List below this h2 header</h2>
<ul>
<li>Hello</li>
</ul>
<div refs="wysiwyg-editor@edit-area" contenteditable="true">
<p id="Content!">Some <strong>content</strong> here</p>
<p>This has a <a href="https://example.com" target="_blank" title="Link to example">link</a> in it</p>
<h2>List below this h2 header</h2>
<ul>
<li>Hello</li>
</ul>
<p class="callout danger">
Hello there, this is an info callout
</p>
<p class="callout danger">
Hello there, this is an info callout
</p>
</div>
</div>
<div id="lexical-debug" style="white-space: pre-wrap; font-size: 12px; height: 200px; overflow-y: scroll; background-color: #000; padding: 1rem; border-radius: 4px; color: #FFF;"></div>