diff --git a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts index 3a9a62dbf..508153dc8 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts @@ -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*/`
@@ -198,15 +194,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem } } - getPreprocessedData() { - const str = this.textEditor - .getData() - .replace(/]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") - .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode - - return $("
").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, href: string) { const { noteId } = linkService.parseNavigationStateFromUrl(href); const note = noteId ? await froca.getNote(noteId, true) : null; diff --git a/apps/client/src/widgets/react/CKEditor.tsx b/apps/client/src/widgets/react/CKEditor.tsx index 60d5f37ab..e9080e310 100644 --- a/apps/client/src/widgets/react/CKEditor.tsx +++ b/apps/client/src/widgets/react/CKEditor.tsx @@ -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); } }} /> diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index 716c0f25c..dc9e1aa1c 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -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 = `

${t("attribute_editor.help_text_body1")}

@@ -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(""); + const [ initialValue, setInitialValue ] = useState(""); + const currentValueRef = useRef(initialValue); const wrapperRef = useRef(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 ( -
- { - console.log("Data changed!"); - }} - onClick={(pos) => { - if (pos && pos.textNode && pos.textNode.data) { - setState("showAttributeDetail") - } else { - setState("showHelpTooltip"); - } - }} - disableNewlines disableSpellcheck - /> -
+ <> +
+ { + 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 + /> +
+ + {attributeDetailWidgetEl} + ) +} + +function getPreprocessedData(currentValue: string) { + const str = currentValue + .replace(/]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1") + .replace(/ /g, " "); // otherwise .text() below outputs non-breaking space in unicode + + return $("
").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; } \ No newline at end of file