mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-26 12:55:29 +01:00
Lexical: Added basic URL field header option list
May show bad option label names on chrome/safari. This was an easy first pass without loads of extra custom UI since we're using native datalists.
This commit is contained in:
parent
1ef4044419
commit
ad6b26ba97
@ -84,6 +84,17 @@ export function uniqueId() {
|
||||
return (`${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random smaller unique ID.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function uniqueIdSmall() {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
const S4 = () => (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
return S4();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a promise that resolves after the given time.
|
||||
* @param {int} timeMs
|
||||
|
@ -30,9 +30,7 @@ export class CustomHeadingNode extends HeadingNode {
|
||||
}
|
||||
|
||||
static clone(node: CustomHeadingNode) {
|
||||
const newNode = new CustomHeadingNode(node.__tag, node.__key);
|
||||
newNode.__id = node.__id;
|
||||
return newNode;
|
||||
return new CustomHeadingNode(node.__tag, node.__key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
## In progress
|
||||
|
||||
- Link heading-based ID reference menu
|
||||
//
|
||||
|
||||
## Main Todo
|
||||
|
||||
|
@ -18,6 +18,7 @@ import {showImageManager} from "../../../utils/images";
|
||||
import searchImageIcon from "@icons/editor/image-search.svg";
|
||||
import searchIcon from "@icons/search.svg";
|
||||
import {showLinkSelector} from "../../../utils/links";
|
||||
import {LinkField} from "../../framework/blocks/link-field";
|
||||
|
||||
export function $showImageForm(image: ImageNode, context: EditorUiContext) {
|
||||
const imageModal: EditorFormModal = context.manager.createModal('image');
|
||||
@ -132,11 +133,11 @@ export const link: EditorFormDefinition = {
|
||||
{
|
||||
build() {
|
||||
return new EditorActionField(
|
||||
new EditorFormField({
|
||||
new LinkField(new EditorFormField({
|
||||
label: 'URL',
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
}),
|
||||
})),
|
||||
new EditorButton({
|
||||
label: 'Browse links',
|
||||
icon: searchIcon,
|
||||
|
@ -1,14 +1,13 @@
|
||||
import {EditorContainerUiElement, EditorUiElement} from "../core";
|
||||
import {el} from "../../../utils/dom";
|
||||
import {EditorFormField} from "../forms";
|
||||
import {EditorButton} from "../buttons";
|
||||
|
||||
|
||||
export class EditorActionField extends EditorContainerUiElement {
|
||||
protected input: EditorFormField;
|
||||
protected input: EditorUiElement;
|
||||
protected action: EditorButton;
|
||||
|
||||
constructor(input: EditorFormField, action: EditorButton) {
|
||||
constructor(input: EditorUiElement, action: EditorButton) {
|
||||
super([input, action]);
|
||||
|
||||
this.input = input;
|
||||
|
96
resources/js/wysiwyg/ui/framework/blocks/link-field.ts
Normal file
96
resources/js/wysiwyg/ui/framework/blocks/link-field.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import {EditorContainerUiElement} from "../core";
|
||||
import {el} from "../../../utils/dom";
|
||||
import {EditorFormField} from "../forms";
|
||||
import {CustomHeadingNode} from "../../../nodes/custom-heading";
|
||||
import {$getAllNodesOfType} from "../../../utils/nodes";
|
||||
import {$isHeadingNode} from "@lexical/rich-text";
|
||||
import {uniqueIdSmall} from "../../../../services/util";
|
||||
|
||||
export class LinkField extends EditorContainerUiElement {
|
||||
protected input: EditorFormField;
|
||||
protected headerMap = new Map<string, CustomHeadingNode>();
|
||||
|
||||
constructor(input: EditorFormField) {
|
||||
super([input]);
|
||||
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
buildDOM(): HTMLElement {
|
||||
const listId = 'editor-form-datalist-' + this.input.getName() + '-' + Date.now();
|
||||
const inputOuterDOM = this.input.getDOMElement();
|
||||
const inputFieldDOM = inputOuterDOM.querySelector('input');
|
||||
inputFieldDOM?.setAttribute('list', listId);
|
||||
inputFieldDOM?.setAttribute('autocomplete', 'off');
|
||||
const datalist = el('datalist', {id: listId});
|
||||
|
||||
const container = el('div', {
|
||||
class: 'editor-link-field-container',
|
||||
}, [inputOuterDOM, datalist]);
|
||||
|
||||
inputFieldDOM?.addEventListener('focusin', () => {
|
||||
this.updateDataList(datalist);
|
||||
});
|
||||
|
||||
inputFieldDOM?.addEventListener('input', () => {
|
||||
const value = inputFieldDOM.value;
|
||||
const header = this.headerMap.get(value);
|
||||
if (header) {
|
||||
this.updateFormFromHeader(header);
|
||||
}
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
updateFormFromHeader(header: CustomHeadingNode) {
|
||||
this.getHeaderIdAndText(header).then(({id, text}) => {
|
||||
console.log('updating form', id, text);
|
||||
const modal = this.getContext().manager.getActiveModal('link');
|
||||
if (modal) {
|
||||
modal.getForm().setValues({
|
||||
url: `#${id}`,
|
||||
text: text,
|
||||
title: text,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getHeaderIdAndText(header: CustomHeadingNode): Promise<{id: string, text: string}> {
|
||||
return new Promise((res) => {
|
||||
this.getContext().editor.update(() => {
|
||||
let id = header.getId();
|
||||
console.log('header', id, header.__id);
|
||||
if (!id) {
|
||||
id = 'header-' + uniqueIdSmall();
|
||||
header.setId(id);
|
||||
}
|
||||
|
||||
const text = header.getTextContent();
|
||||
res({id, text});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateDataList(listEl: HTMLElement) {
|
||||
this.getContext().editor.getEditorState().read(() => {
|
||||
const headers = $getAllNodesOfType($isHeadingNode) as CustomHeadingNode[];
|
||||
|
||||
this.headerMap.clear();
|
||||
const listEls: HTMLElement[] = [];
|
||||
|
||||
for (const header of headers) {
|
||||
const key = 'header-' + header.getKey();
|
||||
this.headerMap.set(key, header);
|
||||
listEls.push(el('option', {
|
||||
value: key,
|
||||
label: header.getTextContent().substring(0, 54),
|
||||
}));
|
||||
}
|
||||
|
||||
listEl.innerHTML = '';
|
||||
listEl.append(...listEls);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import {$getRoot, $isTextNode, LexicalEditor, LexicalNode} from "lexical";
|
||||
import {$getRoot, $isElementNode, $isTextNode, ElementNode, LexicalEditor, LexicalNode} from "lexical";
|
||||
import {LexicalNodeMatcher} from "../nodes";
|
||||
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
||||
import {$generateNodesFromDOM} from "@lexical/html";
|
||||
@ -31,6 +31,26 @@ export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher)
|
||||
return null;
|
||||
}
|
||||
|
||||
export function $getAllNodesOfType(matcher: LexicalNodeMatcher, root?: ElementNode): LexicalNode[] {
|
||||
if (!root) {
|
||||
root = $getRoot();
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
|
||||
for (const child of root.getChildren()) {
|
||||
if (matcher(child)) {
|
||||
matches.push(child);
|
||||
}
|
||||
|
||||
if ($isElementNode(child)) {
|
||||
matches.push(...$getAllNodesOfType(matcher, child));
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nearest root/block level node for the given position.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user