mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	chore(react/ribbon): add logic for displaying attribute detail
This commit is contained in:
		| @@ -1,12 +1,10 @@ | ||||
| import { t } from "../../services/i18n.js"; | ||||
| import NoteContextAwareWidget from "../note_context_aware_widget.js"; | ||||
| import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js"; | ||||
| import server from "../../services/server.js"; | ||||
| import contextMenuService from "../../menus/context_menu.js"; | ||||
| import attributeParser, { type Attribute } from "../../services/attribute_parser.js"; | ||||
| import { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5"; | ||||
| import froca from "../../services/froca.js"; | ||||
| import attributeRenderer from "../../services/attribute_renderer.js"; | ||||
| import noteCreateService from "../../services/note_create.js"; | ||||
| import attributeService from "../../services/attributes.js"; | ||||
| import linkService from "../../services/link.js"; | ||||
| @@ -16,8 +14,6 @@ import type { default as FAttribute, AttributeType } from "../../entities/fattri | ||||
| import type FNote from "../../entities/fnote.js"; | ||||
| import { escapeQuotes } from "../../services/utils.js"; | ||||
|  | ||||
|  | ||||
|  | ||||
| const TPL = /*html*/` | ||||
|  | ||||
|     <div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div> | ||||
| @@ -198,15 +194,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getPreprocessedData() { | ||||
|         const str = this.textEditor | ||||
|             .getData() | ||||
|             .replace(/<a[^>]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") | ||||
|             .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode | ||||
|  | ||||
|         return $("<div>").html(str).text(); | ||||
|     } | ||||
|  | ||||
|     dataChanged() { | ||||
|         this.lastUpdatedNoteId = this.noteId; | ||||
|  | ||||
| @@ -223,64 +210,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async handleEditorClick(e: JQuery.ClickEvent) { | ||||
|         if () { | ||||
|             const clickIndex = this.getClickIndex(pos); | ||||
|  | ||||
|             let parsedAttrs; | ||||
|  | ||||
|             try { | ||||
|                 parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true); | ||||
|             } catch (e) { | ||||
|                 // the input is incorrect because the user messed up with it and now needs to fix it manually | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             let matchedAttr: Attribute | null = null; | ||||
|  | ||||
|             for (const attr of parsedAttrs) { | ||||
|                 if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { | ||||
|                     matchedAttr = attr; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 if (matchedAttr) { | ||||
|                     this.$editor.tooltip("hide"); | ||||
|  | ||||
|                     this.attributeDetailWidget.showAttributeDetail({ | ||||
|                         allAttributes: parsedAttrs, | ||||
|                         attribute: matchedAttr, | ||||
|                         isOwned: true, | ||||
|                         x: e.pageX, | ||||
|                         y: e.pageY | ||||
|                     }); | ||||
|                 } else { | ||||
|                     this.showHelpTooltip(); | ||||
|                 } | ||||
|             }, 100); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getClickIndex(pos: ModelPosition) { | ||||
|         let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); | ||||
|  | ||||
|         let curNode: ModelNode | Text | ModelElement | null = pos.textNode; | ||||
|  | ||||
|         while (curNode?.previousSibling) { | ||||
|             curNode = curNode.previousSibling; | ||||
|  | ||||
|             if ((curNode as ModelElement).name === "reference") { | ||||
|                 clickIndex += (curNode.getAttribute("href") as string).length + 1; | ||||
|             } else if ("data" in curNode) { | ||||
|                 clickIndex += (curNode.data as string).length; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return clickIndex; | ||||
|     } | ||||
|  | ||||
|     async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string) { | ||||
|         const { noteId } = linkService.parseNavigationStateFromUrl(href); | ||||
|         const note = noteId ? await froca.getNote(noteId, true) : null; | ||||
|   | ||||
| @@ -9,8 +9,8 @@ interface CKEditorOpts { | ||||
|     editor: typeof AttributeEditor; | ||||
|     disableNewlines?: boolean; | ||||
|     disableSpellcheck?: boolean; | ||||
|     onChange?: () => void; | ||||
|     onClick?: (pos?: ModelPosition | null) => void; | ||||
|     onChange?: (newValue?: string) => void; | ||||
|     onClick?: (e, pos?: ModelPosition | null) => void; | ||||
| } | ||||
|  | ||||
| export default function CKEditor({ currentValue, className, tabIndex, editor, config, disableNewlines, disableSpellcheck, onChange, onClick }: CKEditorOpts) { | ||||
| @@ -43,7 +43,9 @@ export default function CKEditor({ currentValue, className, tabIndex, editor, co | ||||
|             } | ||||
|  | ||||
|             if (onChange) { | ||||
|                 textEditor.model.document.on("change:data", onChange); | ||||
|                 textEditor.model.document.on("change:data", () => { | ||||
|                     onChange(textEditor.getData()) | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if (currentValue) { | ||||
| @@ -62,10 +64,10 @@ export default function CKEditor({ currentValue, className, tabIndex, editor, co | ||||
|             ref={editorContainerRef} | ||||
|             className={className} | ||||
|             tabIndex={tabIndex} | ||||
|             onClick={() => { | ||||
|             onClick={(e) => { | ||||
|                 if (onClick) { | ||||
|                     const pos = textEditorRef.current?.model.document.selection.getFirstPosition(); | ||||
|                     onClick(pos); | ||||
|                     onClick(e, pos); | ||||
|                 } | ||||
|             }} | ||||
|         /> | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| import { useEffect, useRef, useState } from "preact/hooks" | ||||
| import { AttributeEditor as CKEditorAttributeEditor, MentionFeed } from "@triliumnext/ckeditor5"; | ||||
| import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5"; | ||||
| import { t } from "../../../services/i18n"; | ||||
| import server from "../../../services/server"; | ||||
| import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete"; | ||||
| import CKEditor from "../../react/CKEditor"; | ||||
| import { useTooltip } from "../../react/hooks"; | ||||
| import { useLegacyWidget, useTooltip } from "../../react/hooks"; | ||||
| import FAttribute from "../../../entities/fattribute"; | ||||
| import attribute_renderer from "../../../services/attribute_renderer"; | ||||
| import FNote from "../../../entities/fnote"; | ||||
| import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; | ||||
| import attribute_parser, { Attribute } from "../../../services/attribute_parser"; | ||||
|  | ||||
| const HELP_TEXT = ` | ||||
| <p>${t("attribute_editor.help_text_body1")}</p> | ||||
| @@ -64,7 +66,8 @@ const mentionSetup: MentionFeed[] = [ | ||||
| export default function AttributeEditor({ note }: { note: FNote }) { | ||||
|  | ||||
|     const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">(); | ||||
|     const [ currentValue, setCurrentValue ] = useState<string>(""); | ||||
|     const [ initialValue, setInitialValue ] = useState<string>(""); | ||||
|     const currentValueRef = useRef(initialValue); | ||||
|     const wrapperRef = useRef<HTMLDivElement>(null); | ||||
|     const { showTooltip, hideTooltip } = useTooltip(wrapperRef, { | ||||
|         trigger: "focus", | ||||
| @@ -74,6 +77,8 @@ export default function AttributeEditor({ note }: { note: FNote }) { | ||||
|         offset: "0,30" | ||||
|     }); | ||||
|  | ||||
|     const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget()); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (state === "showHelpTooltip") { | ||||
|             showTooltip(); | ||||
| @@ -92,7 +97,7 @@ export default function AttributeEditor({ note }: { note: FNote }) { | ||||
|             htmlAttrs += " "; | ||||
|         } | ||||
|  | ||||
|         setCurrentValue(htmlAttrs); | ||||
|         setInitialValue(htmlAttrs); | ||||
|     } | ||||
|  | ||||
|     useEffect(() => { | ||||
| @@ -100,30 +105,93 @@ export default function AttributeEditor({ note }: { note: FNote }) { | ||||
|     }, [ note ]); | ||||
|      | ||||
|     return ( | ||||
|         <div ref={wrapperRef} style="position: relative; padding-top: 10px; padding-bottom: 10px"> | ||||
|             <CKEditor | ||||
|                 className="attribute-list-editor" | ||||
|                 tabIndex={200} | ||||
|                 editor={CKEditorAttributeEditor} | ||||
|                 currentValue={currentValue} | ||||
|                 config={{ | ||||
|                     toolbar: { items: [] }, | ||||
|                     placeholder: t("attribute_editor.placeholder"), | ||||
|                     mention: { feeds: mentionSetup }, | ||||
|                     licenseKey: "GPL" | ||||
|                 }} | ||||
|                 onChange={() => { | ||||
|                     console.log("Data changed!"); | ||||
|                 }} | ||||
|                 onClick={(pos) => { | ||||
|                     if (pos && pos.textNode && pos.textNode.data) { | ||||
|                         setState("showAttributeDetail") | ||||
|                     } else { | ||||
|                         setState("showHelpTooltip"); | ||||
|                     } | ||||
|                 }} | ||||
|                 disableNewlines disableSpellcheck | ||||
|             /> | ||||
|         </div> | ||||
|         <> | ||||
|             <div ref={wrapperRef} style="position: relative; padding-top: 10px; padding-bottom: 10px"> | ||||
|                 <CKEditor | ||||
|                     className="attribute-list-editor" | ||||
|                     tabIndex={200} | ||||
|                     editor={CKEditorAttributeEditor} | ||||
|                     currentValue={initialValue} | ||||
|                     config={{ | ||||
|                         toolbar: { items: [] }, | ||||
|                         placeholder: t("attribute_editor.placeholder"), | ||||
|                         mention: { feeds: mentionSetup }, | ||||
|                         licenseKey: "GPL" | ||||
|                     }} | ||||
|                     onChange={(currentValue) => { | ||||
|                         currentValueRef.current = currentValue ?? ""; | ||||
|                     }} | ||||
|                     onClick={(e, pos) => { | ||||
|                         if (pos && pos.textNode && pos.textNode.data) { | ||||
|                             const clickIndex = getClickIndex(pos); | ||||
|  | ||||
|                             let parsedAttrs; | ||||
|  | ||||
|                             try { | ||||
|                                 parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true); | ||||
|                             } catch (e) { | ||||
|                                 // the input is incorrect because the user messed up with it and now needs to fix it manually | ||||
|                                 return null; | ||||
|                             } | ||||
|  | ||||
|                             let matchedAttr: Attribute | null = null; | ||||
|  | ||||
|                             for (const attr of parsedAttrs) { | ||||
|                                 if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) { | ||||
|                                     matchedAttr = attr; | ||||
|                                     break; | ||||
|                                 } | ||||
|                             } | ||||
|  | ||||
|                             setTimeout(() => { | ||||
|                                 if (matchedAttr) { | ||||
|                                     attributeDetailWidget.showAttributeDetail({ | ||||
|                                         allAttributes: parsedAttrs, | ||||
|                                         attribute: matchedAttr, | ||||
|                                         isOwned: true, | ||||
|                                         x: e.pageX, | ||||
|                                         y: e.pageY | ||||
|                                     }); | ||||
|                                     setState("showAttributeDetail");                                     | ||||
|                                 } else { | ||||
|                                     setState("showHelpTooltip"); | ||||
|                                 } | ||||
|                             }, 100); | ||||
|                         } else { | ||||
|                             setState("showHelpTooltip"); | ||||
|                         } | ||||
|                     }} | ||||
|                     disableNewlines disableSpellcheck | ||||
|                 /> | ||||
|             </div> | ||||
|  | ||||
|             {attributeDetailWidgetEl} | ||||
|         </> | ||||
|     )    | ||||
| } | ||||
|  | ||||
| function getPreprocessedData(currentValue: string) { | ||||
|     const str = currentValue | ||||
|         .replace(/<a[^>]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") | ||||
|         .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode | ||||
|  | ||||
|     return $("<div>").html(str).text(); | ||||
| } | ||||
|  | ||||
| function getClickIndex(pos: ModelPosition) { | ||||
|     let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0); | ||||
|  | ||||
|     let curNode: ModelNode | Text | ModelElement | null = pos.textNode; | ||||
|  | ||||
|     while (curNode?.previousSibling) { | ||||
|         curNode = curNode.previousSibling; | ||||
|  | ||||
|         if ((curNode as ModelElement).name === "reference") { | ||||
|             clickIndex += (curNode.getAttribute("href") as string).length + 1; | ||||
|         } else if ("data" in curNode) { | ||||
|             clickIndex += (curNode.data as string).length; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return clickIndex; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user