mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-17 18:02:26 +08:00
Lexical: Added context toolbar placement, added link toolbar
Also added some basic context toolbar styling
This commit is contained in:
1
resources/icons/editor/unlink.svg
Normal file
1
resources/icons/editor/unlink.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>
|
After Width: | Height: | Size: 391 B |
@ -1,7 +1,7 @@
|
|||||||
import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
|
import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
|
||||||
import {
|
import {
|
||||||
$createNodeSelection,
|
$createNodeSelection,
|
||||||
$createParagraphNode, $getRoot, $getSelection,
|
$createParagraphNode, $createTextNode, $getRoot, $getSelection,
|
||||||
$isParagraphNode, $isTextNode, $setSelection,
|
$isParagraphNode, $isTextNode, $setSelection,
|
||||||
BaseSelection, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, ElementNode, FORMAT_TEXT_COMMAND,
|
BaseSelection, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, ElementNode, FORMAT_TEXT_COMMAND,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
@ -45,12 +45,13 @@ import listBulletIcon from "@icons/editor/list-bullet.svg"
|
|||||||
import listNumberedIcon from "@icons/editor/list-numbered.svg"
|
import listNumberedIcon from "@icons/editor/list-numbered.svg"
|
||||||
import listCheckIcon from "@icons/editor/list-check.svg"
|
import listCheckIcon from "@icons/editor/list-check.svg"
|
||||||
import linkIcon from "@icons/editor/link.svg"
|
import linkIcon from "@icons/editor/link.svg"
|
||||||
|
import unlinkIcon from "@icons/editor/unlink.svg"
|
||||||
import tableIcon from "@icons/editor/table.svg"
|
import tableIcon from "@icons/editor/table.svg"
|
||||||
import imageIcon from "@icons/editor/image.svg"
|
import imageIcon from "@icons/editor/image.svg"
|
||||||
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
|
import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"
|
||||||
import detailsIcon from "@icons/editor/details.svg"
|
import detailsIcon from "@icons/editor/details.svg"
|
||||||
import sourceIcon from "@icons/editor/source-view.svg"
|
import sourceIcon from "@icons/editor/source-view.svg"
|
||||||
import {$createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode} from "../../nodes/horizontal-rule";
|
import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule";
|
||||||
|
|
||||||
export const undo: EditorButtonDefinition = {
|
export const undo: EditorButtonDefinition = {
|
||||||
label: 'Undo',
|
label: 'Undo',
|
||||||
@ -258,6 +259,31 @@ export const link: EditorButtonDefinition = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const unlink: EditorButtonDefinition = {
|
||||||
|
label: 'Remove link',
|
||||||
|
icon: unlinkIcon,
|
||||||
|
action(context: EditorUiContext) {
|
||||||
|
context.editor.update(() => {
|
||||||
|
const selection = context.lastSelection;
|
||||||
|
const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
|
||||||
|
const selectionPoints = selection?.getStartEndPoints();
|
||||||
|
|
||||||
|
if (selectedLink) {
|
||||||
|
const newNode = $createTextNode(selectedLink.getTextContent());
|
||||||
|
selectedLink.replace(newNode);
|
||||||
|
if (selectionPoints?.length === 2) {
|
||||||
|
newNode.select(selectionPoints[0].offset, selectionPoints[1].offset);
|
||||||
|
} else {
|
||||||
|
newNode.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
isActive(selection: BaseSelection|null): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const table: EditorBasicButtonDefinition = {
|
export const table: EditorBasicButtonDefinition = {
|
||||||
label: 'Table',
|
label: 'Table',
|
||||||
icon: tableIcon,
|
icon: tableIcon,
|
||||||
|
@ -94,6 +94,7 @@ export class EditorUIManager {
|
|||||||
for (const toolbar of this.activeContextToolbars) {
|
for (const toolbar of this.activeContextToolbars) {
|
||||||
toolbar.updateState(update);
|
toolbar.updateState(update);
|
||||||
}
|
}
|
||||||
|
// console.log('selection update', update.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateContextToolbars(update: EditorUiStateUpdate): void {
|
protected updateContextToolbars(update: EditorUiStateUpdate): void {
|
||||||
|
@ -16,8 +16,14 @@ export class EditorContextToolbar extends EditorContainerUiElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attachTo(target: HTMLElement) {
|
attachTo(target: HTMLElement) {
|
||||||
// Todo - attach to target position
|
const targetBounds = target.getBoundingClientRect();
|
||||||
console.log('attaching context toolbar to', target);
|
const dom = this.getDOMElement();
|
||||||
|
const domBounds = dom.getBoundingClientRect();
|
||||||
|
|
||||||
|
const targetMid = targetBounds.left + (targetBounds.width / 2);
|
||||||
|
const targetLeft = targetMid - (domBounds.width / 2);
|
||||||
|
dom.style.top = (targetBounds.bottom + 6) + 'px';
|
||||||
|
dom.style.left = targetLeft + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(children: EditorUiElement[]) {
|
insert(children: EditorUiElement[]) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {LexicalEditor} from "lexical";
|
import {LexicalEditor} from "lexical";
|
||||||
import {getImageToolbarContent, getMainEditorFullToolbar} from "./toolbars";
|
import {getImageToolbarContent, getLinkToolbarContent, getMainEditorFullToolbar} from "./toolbars";
|
||||||
import {EditorUIManager} from "./framework/manager";
|
import {EditorUIManager} from "./framework/manager";
|
||||||
import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
|
import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions";
|
||||||
import {ImageDecorator} from "./decorators/image";
|
import {ImageDecorator} from "./decorators/image";
|
||||||
@ -41,6 +41,10 @@ export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
|
|||||||
return originalTarget.closest('a') || originalTarget;
|
return originalTarget.closest('a') || originalTarget;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
manager.registerContextToolbar('link', {
|
||||||
|
selector: 'a',
|
||||||
|
content: getLinkToolbarContent(),
|
||||||
|
});
|
||||||
|
|
||||||
// Register image decorator listener
|
// Register image decorator listener
|
||||||
manager.registerDecoratorType('image', ImageDecorator);
|
manager.registerDecoratorType('image', ImageDecorator);
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
infoCallout, italic, link, numberList, paragraph,
|
infoCallout, italic, link, numberList, paragraph,
|
||||||
redo, source, strikethrough, subscript,
|
redo, source, strikethrough, subscript,
|
||||||
successCallout, superscript, table, taskList, textColor, underline,
|
successCallout, superscript, table, taskList, textColor, underline,
|
||||||
undo,
|
undo, unlink,
|
||||||
warningCallout
|
warningCallout
|
||||||
} from "./defaults/button-definitions";
|
} from "./defaults/button-definitions";
|
||||||
import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext, EditorUiElement} from "./framework/core";
|
import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext, EditorUiElement} from "./framework/core";
|
||||||
@ -92,3 +92,10 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
|
|||||||
export function getImageToolbarContent(): EditorUiElement[] {
|
export function getImageToolbarContent(): EditorUiElement[] {
|
||||||
return [new EditorButton(image)];
|
return [new EditorButton(image)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLinkToolbarContent(): EditorUiElement[] {
|
||||||
|
return [
|
||||||
|
new EditorButton(link),
|
||||||
|
new EditorButton(unlink),
|
||||||
|
];
|
||||||
|
}
|
@ -69,6 +69,30 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-context-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
background-color: #FFF;
|
||||||
|
border: 1px solid #DDD;
|
||||||
|
padding: .2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12);
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
z-index: -1;
|
||||||
|
display: block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-top: 1px solid #DDD;
|
||||||
|
border-left: 1px solid #DDD;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -4px;
|
||||||
|
top: -5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Modals
|
// Modals
|
||||||
.editor-modal-wrapper {
|
.editor-modal-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
Reference in New Issue
Block a user