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; | ||||
|  |  | |||
|  | @ -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