diff --git a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts index 508153dc8..0d173cf92 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_editor.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_editor.ts @@ -16,7 +16,6 @@ import { escapeQuotes } from "../../services/utils.js"; const TPL = /*html*/` -
@@ -49,11 +48,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.initialized = this.initEditor(); this.$editor.on("keydown", async (e) => { - if (e.which === 13) { - // allow autocomplete to fill the result textarea - setTimeout(() => this.save(), 100); - } - this.attributeDetailWidget.hide(); }); @@ -62,7 +56,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button"); this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e)); - this.$saveAttributesButton = this.$widget.find(".save-attributes-button"); this.$saveAttributesButton.on("click", () => this.save()); this.$errors = this.$widget.find(".attribute-errors"); @@ -170,39 +163,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem console.warn("Ignoring blur event because a different note is loaded."); return; } - - const attributes = this.parseAttributes(); - - if (attributes) { - await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId); - - this.$saveAttributesButton.fadeOut(); - - // blink the attribute text to give a visual hint that save has been executed - this.$editor.css("opacity", 0); - - // revert back - setTimeout(() => this.$editor.css("opacity", 1), 100); - } - } - - parseAttributes() { - try { - return attributeParser.lexAndParse(this.getPreprocessedData()); - } catch (e: any) { - this.$errors.text(e.message).slideDown(); - } } dataChanged() { this.lastUpdatedNoteId = this.noteId; - if (this.lastSavedContent === this.textEditor.getData()) { - this.$saveAttributesButton.fadeOut(); - } else { - this.$saveAttributesButton.fadeIn(); - } - if (this.$errors.is(":visible")) { // using .hide() instead of .slideUp() since this will also hide the error after confirming // mention for relation name which suits up. When using.slideUp() error will appear and the slideUp which is weird @@ -218,14 +183,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem $el.text(title); } - async renderOwnedAttributes(ownedAttributes: FAttribute[], saved: boolean) { - if (saved) { - this.lastSavedContent = this.textEditor.getData(); - - this.$saveAttributesButton.fadeOut(0); - } - } - async createNoteForReferenceLink(title: string) { let result; if (this.notePath) { diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index dc9e1aa1c..c1e59b114 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -10,6 +10,8 @@ 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"; +import ActionButton from "../../react/ActionButton"; +import { escapeQuotes } from "../../../services/utils"; const HELP_TEXT = `

${t("attribute_editor.help_text_body1")}

@@ -63,10 +65,13 @@ const mentionSetup: MentionFeed[] = [ ]; -export default function AttributeEditor({ note }: { note: FNote }) { +export default function AttributeEditor({ note, componentId }: { note: FNote, componentId: string }) { const [ state, setState ] = useState<"normal" | "showHelpTooltip" | "showAttributeDetail">(); + const [ error, setError ] = useState(); + const [ needsSaving, setNeedsSaving ] = useState(false); const [ initialValue, setInitialValue ] = useState(""); + const lastSavedContent = useRef(); const currentValueRef = useRef(initialValue); const wrapperRef = useRef(null); const { showTooltip, hideTooltip } = useTooltip(wrapperRef, { @@ -97,16 +102,55 @@ export default function AttributeEditor({ note }: { note: FNote }) { htmlAttrs += " "; } + if (saved) { + lastSavedContent.current = currentValueRef.current; + setNeedsSaving(false); + } + setInitialValue(htmlAttrs); } + function parseAttributes() { + try { + return attribute_parser.lexAndParse(getPreprocessedData(currentValueRef.current)); + } catch (e: any) { + setError(e); + } + } + + async function save() { + const attributes = parseAttributes(); + if (!attributes) { + // An error occurred and will be reported to the user. + return; + } + + await server.put(`notes/${note.noteId}/attributes`, attributes, componentId); + setNeedsSaving(false); + + // blink the attribute text to give a visual hint that save has been executed + if (wrapperRef.current) { + wrapperRef.current.style.opacity = "0"; + setTimeout(() => wrapperRef.current!.style.opacity = "1", 100); + } + } + useEffect(() => { renderOwnedAttributes(note.getOwnedAttributes(), true); }, [ note ]); return ( <> -
+
{ + if (e.key === "Enter") { + // allow autocomplete to fill the result textarea + setTimeout(() => save(), 100); + } + }} + > { currentValueRef.current = currentValue ?? ""; + setNeedsSaving(lastSavedContent.current !== currentValue); }} onClick={(e, pos) => { if (pos && pos.textNode && pos.textNode.data) { @@ -163,6 +208,13 @@ export default function AttributeEditor({ note }: { note: FNote }) { }} disableNewlines disableSpellcheck /> + + { needsSaving && }
{attributeDetailWidgetEl}