mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Started table menu options
Updated UI elements to handle new scenarios needed in more complex table menu
This commit is contained in:
parent
13f8f39dd5
commit
6b06d490c5
@ -1,4 +1,4 @@
|
|||||||
import {$isListNode, ListItemNode, ListNode, SerializedListItemNode} from "@lexical/list";
|
import {$isListNode, ListItemNode, SerializedListItemNode} from "@lexical/list";
|
||||||
import {EditorConfig} from "lexical/LexicalEditor";
|
import {EditorConfig} from "lexical/LexicalEditor";
|
||||||
import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
|
import {DOMExportOutput, LexicalEditor, LexicalNode} from "lexical";
|
||||||
import {el} from "../helpers";
|
import {el} from "../helpers";
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
//
|
- Table features
|
||||||
|
- Continued table dropdown menu
|
||||||
|
|
||||||
## Main Todo
|
## Main Todo
|
||||||
|
|
||||||
- Alignments: Use existing classes for blocks
|
- Alignments: Use existing classes for blocks
|
||||||
- Alignments: Handle inline block content (image, video)
|
- Alignments: Handle inline block content (image, video)
|
||||||
- Table features
|
|
||||||
- Image paste upload
|
- Image paste upload
|
||||||
- Keyboard shortcuts support
|
- Keyboard shortcuts support
|
||||||
- Add ID support to all block types
|
- Add ID support to all block types
|
||||||
|
@ -8,17 +8,18 @@ import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg
|
|||||||
import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
|
import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
|
||||||
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
|
||||||
import {EditorUiContext} from "../../framework/core";
|
import {EditorUiContext} from "../../framework/core";
|
||||||
import {$getBlockElementNodesInSelection, $getNodeFromSelection, $getParentOfType} from "../../../helpers";
|
|
||||||
import {$getSelection} from "lexical";
|
|
||||||
import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table";
|
|
||||||
import {
|
import {
|
||||||
$deleteTableColumn, $deleteTableColumn__EXPERIMENTAL,
|
$getNodeFromSelection,
|
||||||
|
$selectionContainsNodeType
|
||||||
|
} from "../../../helpers";
|
||||||
|
import {$getSelection} from "lexical";
|
||||||
|
import {$isCustomTableNode} from "../../../nodes/custom-table";
|
||||||
|
import {
|
||||||
|
$deleteTableColumn__EXPERIMENTAL,
|
||||||
$deleteTableRow__EXPERIMENTAL,
|
$deleteTableRow__EXPERIMENTAL,
|
||||||
$getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL,
|
$insertTableColumn__EXPERIMENTAL,
|
||||||
$insertTableRow, $insertTableRow__EXPERIMENTAL,
|
$insertTableRow__EXPERIMENTAL,
|
||||||
$isTableCellNode,
|
$isTableNode,
|
||||||
$isTableRowNode,
|
|
||||||
TableCellNode
|
|
||||||
} from "@lexical/table";
|
} from "@lexical/table";
|
||||||
|
|
||||||
|
|
||||||
@ -43,6 +44,14 @@ export const deleteTable: EditorButtonDefinition = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteTableMenuAction: EditorButtonDefinition = {
|
||||||
|
...deleteTable,
|
||||||
|
format: 'long',
|
||||||
|
isDisabled(selection) {
|
||||||
|
return !$selectionContainsNodeType(selection, $isTableNode);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const insertRowAbove: EditorButtonDefinition = {
|
export const insertRowAbove: EditorButtonDefinition = {
|
||||||
label: 'Insert row above',
|
label: 'Insert row above',
|
||||||
icon: insertRowAboveIcon,
|
icon: insertRowAboveIcon,
|
||||||
|
@ -3,22 +3,34 @@ import {handleDropdown} from "../helpers/dropdowns";
|
|||||||
import {EditorContainerUiElement, EditorUiElement} from "../core";
|
import {EditorContainerUiElement, EditorUiElement} from "../core";
|
||||||
import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
|
import {EditorBasicButtonDefinition, EditorButton} from "../buttons";
|
||||||
|
|
||||||
|
export type EditorDropdownButtonOptions = {
|
||||||
|
showOnHover?: boolean;
|
||||||
|
direction?: 'vertical'|'horizontal';
|
||||||
|
button: EditorBasicButtonDefinition|EditorButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOptions: EditorDropdownButtonOptions = {
|
||||||
|
showOnHover: false,
|
||||||
|
direction: 'horizontal',
|
||||||
|
button: {label: 'Menu'},
|
||||||
|
}
|
||||||
|
|
||||||
export class EditorDropdownButton extends EditorContainerUiElement {
|
export class EditorDropdownButton extends EditorContainerUiElement {
|
||||||
protected button: EditorButton;
|
protected button: EditorButton;
|
||||||
protected childItems: EditorUiElement[];
|
protected childItems: EditorUiElement[];
|
||||||
protected open: boolean = false;
|
protected open: boolean = false;
|
||||||
protected showOnHover: boolean = false;
|
protected options: EditorDropdownButtonOptions;
|
||||||
|
|
||||||
constructor(button: EditorBasicButtonDefinition|EditorButton, showOnHover: boolean, children: EditorUiElement[]) {
|
constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) {
|
||||||
super(children);
|
super(children);
|
||||||
this.childItems = children;
|
this.childItems = children;
|
||||||
this.showOnHover = showOnHover;
|
this.options = Object.assign(defaultOptions, options);
|
||||||
|
|
||||||
if (button instanceof EditorButton) {
|
if (options.button instanceof EditorButton) {
|
||||||
this.button = button;
|
this.button = options.button;
|
||||||
} else {
|
} else {
|
||||||
this.button = new EditorButton({
|
this.button = new EditorButton({
|
||||||
...button,
|
...options.button,
|
||||||
action() {
|
action() {
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
@ -41,7 +53,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
|
|||||||
|
|
||||||
const childElements: HTMLElement[] = this.childItems.map(child => child.getDOMElement());
|
const childElements: HTMLElement[] = this.childItems.map(child => child.getDOMElement());
|
||||||
const menu = el('div', {
|
const menu = el('div', {
|
||||||
class: 'editor-dropdown-menu',
|
class: `editor-dropdown-menu editor-dropdown-menu-${this.options.direction}`,
|
||||||
hidden: 'true',
|
hidden: 'true',
|
||||||
}, childElements);
|
}, childElements);
|
||||||
|
|
||||||
@ -50,7 +62,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
|
|||||||
}, [button, menu]);
|
}, [button, menu]);
|
||||||
|
|
||||||
handleDropdown({toggle : button, menu : menu,
|
handleDropdown({toggle : button, menu : menu,
|
||||||
showOnHover: this.showOnHover,
|
showOnHover: this.options.showOnHover,
|
||||||
onOpen : () => {
|
onOpen : () => {
|
||||||
this.open = true;
|
this.open = true;
|
||||||
this.getContext().manager.triggerStateUpdateForElement(this.button);
|
this.getContext().manager.triggerStateUpdateForElement(this.button);
|
||||||
|
@ -7,7 +7,7 @@ export class EditorFormatMenu extends EditorContainerUiElement {
|
|||||||
buildDOM(): HTMLElement {
|
buildDOM(): HTMLElement {
|
||||||
const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
|
const childElements: HTMLElement[] = this.getChildren().map(child => child.getDOMElement());
|
||||||
const menu = el('div', {
|
const menu = el('div', {
|
||||||
class: 'editor-format-menu-dropdown editor-dropdown-menu editor-menu-list',
|
class: 'editor-format-menu-dropdown editor-dropdown-menu editor-dropdown-menu-vertical',
|
||||||
hidden: 'true',
|
hidden: 'true',
|
||||||
}, childElements);
|
}, childElements);
|
||||||
|
|
||||||
|
@ -15,9 +15,11 @@ export class EditorOverflowContainer extends EditorContainerUiElement {
|
|||||||
this.size = size;
|
this.size = size;
|
||||||
this.content = children;
|
this.content = children;
|
||||||
this.overflowButton = new EditorDropdownButton({
|
this.overflowButton = new EditorDropdownButton({
|
||||||
label: 'More',
|
button: {
|
||||||
icon: moreHorizontal,
|
label: 'More',
|
||||||
}, false, []);
|
icon: moreHorizontal,
|
||||||
|
},
|
||||||
|
}, []);
|
||||||
this.addChildren(this.overflowButton);
|
this.addChildren(this.overflowButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,13 @@ import {el} from "../../helpers";
|
|||||||
export interface EditorBasicButtonDefinition {
|
export interface EditorBasicButtonDefinition {
|
||||||
label: string;
|
label: string;
|
||||||
icon?: string|undefined;
|
icon?: string|undefined;
|
||||||
|
format?: 'small' | 'long';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
|
export interface EditorButtonDefinition extends EditorBasicButtonDefinition {
|
||||||
action: (context: EditorUiContext, button: EditorButton) => void;
|
action: (context: EditorUiContext, button: EditorButton) => void;
|
||||||
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
isActive: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
||||||
|
isDisabled?: (selection: BaseSelection|null, context: EditorUiContext) => boolean;
|
||||||
setup?: (context: EditorUiContext, button: EditorButton) => void;
|
setup?: (context: EditorUiContext, button: EditorButton) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,20 +49,27 @@ export class EditorButton extends EditorUiElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected buildDOM(): HTMLButtonElement {
|
protected buildDOM(): HTMLButtonElement {
|
||||||
|
|
||||||
const label = this.getLabel();
|
const label = this.getLabel();
|
||||||
let child: string|HTMLElement = label;
|
const format = this.definition.format || 'small';
|
||||||
if (this.definition.icon) {
|
const children: (string|HTMLElement)[] = [];
|
||||||
child = el('div', {class: 'editor-button-icon'});
|
|
||||||
child.innerHTML = this.definition.icon;
|
if (this.definition.icon || format === 'long') {
|
||||||
|
const icon = el('div', {class: 'editor-button-icon'});
|
||||||
|
icon.innerHTML = this.definition.icon || '';
|
||||||
|
children.push(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.definition.icon ||format === 'long') {
|
||||||
|
const text = el('div', {class: 'editor-button-text'}, [label]);
|
||||||
|
children.push(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = el('button', {
|
const button = el('button', {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
class: 'editor-button',
|
class: `editor-button editor-button-${format}`,
|
||||||
title: this.definition.icon ? label : null,
|
title: this.definition.icon ? label : null,
|
||||||
disabled: this.disabled ? 'true' : null,
|
disabled: this.disabled ? 'true' : null,
|
||||||
}, [child]) as HTMLButtonElement;
|
}, children) as HTMLButtonElement;
|
||||||
|
|
||||||
button.addEventListener('click', this.onClick.bind(this));
|
button.addEventListener('click', this.onClick.bind(this));
|
||||||
|
|
||||||
@ -71,11 +80,18 @@ export class EditorButton extends EditorUiElement {
|
|||||||
this.definition.action(this.getContext(), this);
|
this.definition.action(this.getContext(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActiveState(selection: BaseSelection|null) {
|
protected updateActiveState(selection: BaseSelection|null) {
|
||||||
const isActive = this.definition.isActive(selection, this.getContext());
|
const isActive = this.definition.isActive(selection, this.getContext());
|
||||||
this.setActiveState(isActive);
|
this.setActiveState(isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updateDisabledState(selection: BaseSelection|null) {
|
||||||
|
if (this.definition.isDisabled) {
|
||||||
|
const isDisabled = this.definition.isDisabled(selection, this.getContext());
|
||||||
|
this.toggleDisabled(isDisabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setActiveState(active: boolean) {
|
setActiveState(active: boolean) {
|
||||||
this.active = active;
|
this.active = active;
|
||||||
this.dom?.classList.toggle('editor-button-active', this.active);
|
this.dom?.classList.toggle('editor-button-active', this.active);
|
||||||
@ -83,6 +99,7 @@ export class EditorButton extends EditorUiElement {
|
|||||||
|
|
||||||
updateState(state: EditorUiStateUpdate): void {
|
updateState(state: EditorUiStateUpdate): void {
|
||||||
this.updateActiveState(state.selection);
|
this.updateActiveState(state.selection);
|
||||||
|
this.updateDisabledState(state.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive(): boolean {
|
isActive(): boolean {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {EditorButton} from "./framework/buttons";
|
import {EditorButton} from "./framework/buttons";
|
||||||
import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core";
|
import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core";
|
||||||
import {el} from "../helpers";
|
import {$selectionContainsNodeType, el} from "../helpers";
|
||||||
import {EditorFormatMenu} from "./framework/blocks/format-menu";
|
import {EditorFormatMenu} from "./framework/blocks/format-menu";
|
||||||
import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
|
import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
|
||||||
import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
|
import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
|
||||||
@ -11,7 +11,7 @@ import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
|
|||||||
import {
|
import {
|
||||||
deleteColumn,
|
deleteColumn,
|
||||||
deleteRow,
|
deleteRow,
|
||||||
deleteTable, insertColumnAfter,
|
deleteTable, deleteTableMenuAction, insertColumnAfter,
|
||||||
insertColumnBefore,
|
insertColumnBefore,
|
||||||
insertRowAbove,
|
insertRowAbove,
|
||||||
insertRowBelow,
|
insertRowBelow,
|
||||||
@ -50,6 +50,7 @@ import {
|
|||||||
link, media,
|
link, media,
|
||||||
unlink
|
unlink
|
||||||
} from "./defaults/buttons/objects";
|
} from "./defaults/buttons/objects";
|
||||||
|
import {$isTableNode} from "@lexical/table";
|
||||||
|
|
||||||
export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
||||||
return new EditorSimpleClassContainer('editor-toolbar-main', [
|
return new EditorSimpleClassContainer('editor-toolbar-main', [
|
||||||
@ -68,7 +69,7 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
|||||||
new FormatPreviewButton(el('h5'), h5),
|
new FormatPreviewButton(el('h5'), h5),
|
||||||
new FormatPreviewButton(el('blockquote'), blockquote),
|
new FormatPreviewButton(el('blockquote'), blockquote),
|
||||||
new FormatPreviewButton(el('p'), paragraph),
|
new FormatPreviewButton(el('p'), paragraph),
|
||||||
new EditorDropdownButton({label: 'Callouts'}, true, [
|
new EditorDropdownButton({button: {label: 'Callouts'}, showOnHover: true, direction: 'vertical'}, [
|
||||||
new FormatPreviewButton(el('p', {class: 'callout info'}), infoCallout),
|
new FormatPreviewButton(el('p', {class: 'callout info'}), infoCallout),
|
||||||
new FormatPreviewButton(el('p', {class: 'callout success'}), successCallout),
|
new FormatPreviewButton(el('p', {class: 'callout success'}), successCallout),
|
||||||
new FormatPreviewButton(el('p', {class: 'callout warning'}), warningCallout),
|
new FormatPreviewButton(el('p', {class: 'callout warning'}), warningCallout),
|
||||||
@ -81,10 +82,10 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
|||||||
new EditorButton(bold),
|
new EditorButton(bold),
|
||||||
new EditorButton(italic),
|
new EditorButton(italic),
|
||||||
new EditorButton(underline),
|
new EditorButton(underline),
|
||||||
new EditorDropdownButton(new EditorColorButton(textColor, 'color'), false, [
|
new EditorDropdownButton({ button: new EditorColorButton(textColor, 'color') }, [
|
||||||
new EditorColorPicker('color'),
|
new EditorColorPicker('color'),
|
||||||
]),
|
]),
|
||||||
new EditorDropdownButton(new EditorColorButton(highlightColor, 'background-color'), false, [
|
new EditorDropdownButton({button: new EditorColorButton(highlightColor, 'background-color')}, [
|
||||||
new EditorColorPicker('background-color'),
|
new EditorColorPicker('background-color'),
|
||||||
]),
|
]),
|
||||||
new EditorButton(strikethrough),
|
new EditorButton(strikethrough),
|
||||||
@ -112,9 +113,14 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
|||||||
// Insert types
|
// Insert types
|
||||||
new EditorOverflowContainer(8, [
|
new EditorOverflowContainer(8, [
|
||||||
new EditorButton(link),
|
new EditorButton(link),
|
||||||
new EditorDropdownButton(table, false, [
|
|
||||||
new EditorTableCreator(),
|
new EditorDropdownButton({button: table, direction: 'vertical'}, [
|
||||||
|
new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
|
||||||
|
new EditorTableCreator(),
|
||||||
|
]),
|
||||||
|
new EditorButton(deleteTableMenuAction),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
new EditorButton(image),
|
new EditorButton(image),
|
||||||
new EditorButton(horizontalRule),
|
new EditorButton(horizontalRule),
|
||||||
new EditorButton(codeBlock),
|
new EditorButton(codeBlock),
|
||||||
|
@ -59,6 +59,18 @@ body.editor-is-fullscreen {
|
|||||||
background-color: #ceebff;
|
background-color: #ceebff;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
.editor-button-long {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
.editor-button-text {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #000;
|
||||||
|
font-size: 12.2px;
|
||||||
|
}
|
||||||
.editor-button-format-preview {
|
.editor-button-format-preview {
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
display: block;
|
display: block;
|
||||||
@ -84,21 +96,20 @@ body.editor-is-fullscreen {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.editor-menu-list {
|
.editor-dropdown-menu-vertical {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.editor-menu-list .editor-button {
|
.editor-dropdown-menu-vertical .editor-button {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.editor-menu-list > .editor-dropdown-menu-container .editor-dropdown-menu {
|
.editor-dropdown-menu-vertical > .editor-dropdown-menu-container .editor-dropdown-menu {
|
||||||
inset-inline-start: 100%;
|
inset-inline-start: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-format-menu-toggle {
|
.editor-format-menu-toggle {
|
||||||
|
Loading…
Reference in New Issue
Block a user