").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