mirror of
https://github.com/zadam/trilium.git
synced 2025-10-21 15:49:00 +02:00
chore(react/ribbon): add logic for displaying attribute detail
This commit is contained in:
parent
e5caf37697
commit
62372ed4c5
@ -1,12 +1,10 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import NoteContextAwareWidget from "../note_context_aware_widget.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 server from "../../services/server.js";
|
||||||
import contextMenuService from "../../menus/context_menu.js";
|
import contextMenuService from "../../menus/context_menu.js";
|
||||||
import attributeParser, { type Attribute } from "../../services/attribute_parser.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 { AttributeEditor, type EditorConfig, type ModelElement, type MentionFeed, type ModelNode, type ModelPosition } from "@triliumnext/ckeditor5";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
|
||||||
import noteCreateService from "../../services/note_create.js";
|
import noteCreateService from "../../services/note_create.js";
|
||||||
import attributeService from "../../services/attributes.js";
|
import attributeService from "../../services/attributes.js";
|
||||||
import linkService from "../../services/link.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 type FNote from "../../entities/fnote.js";
|
||||||
import { escapeQuotes } from "../../services/utils.js";
|
import { escapeQuotes } from "../../services/utils.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
|
|
||||||
<div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
|
<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() {
|
dataChanged() {
|
||||||
this.lastUpdatedNoteId = this.noteId;
|
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) {
|
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string) {
|
||||||
const { noteId } = linkService.parseNavigationStateFromUrl(href);
|
const { noteId } = linkService.parseNavigationStateFromUrl(href);
|
||||||
const note = noteId ? await froca.getNote(noteId, true) : null;
|
const note = noteId ? await froca.getNote(noteId, true) : null;
|
||||||
|
@ -9,8 +9,8 @@ interface CKEditorOpts {
|
|||||||
editor: typeof AttributeEditor;
|
editor: typeof AttributeEditor;
|
||||||
disableNewlines?: boolean;
|
disableNewlines?: boolean;
|
||||||
disableSpellcheck?: boolean;
|
disableSpellcheck?: boolean;
|
||||||
onChange?: () => void;
|
onChange?: (newValue?: string) => void;
|
||||||
onClick?: (pos?: ModelPosition | null) => void;
|
onClick?: (e, pos?: ModelPosition | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CKEditor({ currentValue, className, tabIndex, editor, config, disableNewlines, disableSpellcheck, onChange, onClick }: CKEditorOpts) {
|
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) {
|
if (onChange) {
|
||||||
textEditor.model.document.on("change:data", onChange);
|
textEditor.model.document.on("change:data", () => {
|
||||||
|
onChange(textEditor.getData())
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentValue) {
|
if (currentValue) {
|
||||||
@ -62,10 +64,10 @@ export default function CKEditor({ currentValue, className, tabIndex, editor, co
|
|||||||
ref={editorContainerRef}
|
ref={editorContainerRef}
|
||||||
className={className}
|
className={className}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
const pos = textEditorRef.current?.model.document.selection.getFirstPosition();
|
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 { 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 { t } from "../../../services/i18n";
|
||||||
import server from "../../../services/server";
|
import server from "../../../services/server";
|
||||||
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
|
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
|
||||||
import CKEditor from "../../react/CKEditor";
|
import CKEditor from "../../react/CKEditor";
|
||||||
import { useTooltip } from "../../react/hooks";
|
import { useLegacyWidget, useTooltip } from "../../react/hooks";
|
||||||
import FAttribute from "../../../entities/fattribute";
|
import FAttribute from "../../../entities/fattribute";
|
||||||
import attribute_renderer from "../../../services/attribute_renderer";
|
import attribute_renderer from "../../../services/attribute_renderer";
|
||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
|
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
|
||||||
|
import attribute_parser, { Attribute } from "../../../services/attribute_parser";
|
||||||
|
|
||||||
const HELP_TEXT = `
|
const HELP_TEXT = `
|
||||||
<p>${t("attribute_editor.help_text_body1")}</p>
|
<p>${t("attribute_editor.help_text_body1")}</p>
|
||||||
@ -64,7 +66,8 @@ const mentionSetup: MentionFeed[] = [
|
|||||||
export default function AttributeEditor({ note }: { note: FNote }) {
|
export default function AttributeEditor({ note }: { note: FNote }) {
|
||||||
|
|
||||||
const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">();
|
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 wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const { showTooltip, hideTooltip } = useTooltip(wrapperRef, {
|
const { showTooltip, hideTooltip } = useTooltip(wrapperRef, {
|
||||||
trigger: "focus",
|
trigger: "focus",
|
||||||
@ -74,6 +77,8 @@ export default function AttributeEditor({ note }: { note: FNote }) {
|
|||||||
offset: "0,30"
|
offset: "0,30"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state === "showHelpTooltip") {
|
if (state === "showHelpTooltip") {
|
||||||
showTooltip();
|
showTooltip();
|
||||||
@ -92,7 +97,7 @@ export default function AttributeEditor({ note }: { note: FNote }) {
|
|||||||
htmlAttrs += " ";
|
htmlAttrs += " ";
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentValue(htmlAttrs);
|
setInitialValue(htmlAttrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -100,30 +105,93 @@ export default function AttributeEditor({ note }: { note: FNote }) {
|
|||||||
}, [ note ]);
|
}, [ note ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={wrapperRef} style="position: relative; padding-top: 10px; padding-bottom: 10px">
|
<>
|
||||||
<CKEditor
|
<div ref={wrapperRef} style="position: relative; padding-top: 10px; padding-bottom: 10px">
|
||||||
className="attribute-list-editor"
|
<CKEditor
|
||||||
tabIndex={200}
|
className="attribute-list-editor"
|
||||||
editor={CKEditorAttributeEditor}
|
tabIndex={200}
|
||||||
currentValue={currentValue}
|
editor={CKEditorAttributeEditor}
|
||||||
config={{
|
currentValue={initialValue}
|
||||||
toolbar: { items: [] },
|
config={{
|
||||||
placeholder: t("attribute_editor.placeholder"),
|
toolbar: { items: [] },
|
||||||
mention: { feeds: mentionSetup },
|
placeholder: t("attribute_editor.placeholder"),
|
||||||
licenseKey: "GPL"
|
mention: { feeds: mentionSetup },
|
||||||
}}
|
licenseKey: "GPL"
|
||||||
onChange={() => {
|
}}
|
||||||
console.log("Data changed!");
|
onChange={(currentValue) => {
|
||||||
}}
|
currentValueRef.current = currentValue ?? "";
|
||||||
onClick={(pos) => {
|
}}
|
||||||
if (pos && pos.textNode && pos.textNode.data) {
|
onClick={(e, pos) => {
|
||||||
setState("showAttributeDetail")
|
if (pos && pos.textNode && pos.textNode.data) {
|
||||||
} else {
|
const clickIndex = getClickIndex(pos);
|
||||||
setState("showHelpTooltip");
|
|
||||||
}
|
let parsedAttrs;
|
||||||
}}
|
|
||||||
disableNewlines disableSpellcheck
|
try {
|
||||||
/>
|
parsedAttrs = attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current), true);
|
||||||
</div>
|
} 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;
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user