chore(react/ribbon): start implementing attribute editor

This commit is contained in:
Elian Doran 2025-08-23 11:44:51 +03:00
parent 2c33ef2b0d
commit 6d37e19b40
No known key found for this signature in database
6 changed files with 157 additions and 155 deletions

View File

@ -24,58 +24,6 @@ const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body3")}</p>`;
const TPL = /*html*/`
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
<style>
.attribute-list-editor {
border: 0 !important;
outline: 0 !important;
box-shadow: none !important;
padding: 0 0 0 5px !important;
margin: 0 !important;
max-height: 100px;
overflow: auto;
transition: opacity .1s linear;
}
.attribute-list-editor.ck-content .mention {
color: var(--muted-text-color) !important;
background: transparent !important;
}
.save-attributes-button {
color: var(--muted-text-color);
position: absolute;
bottom: 14px;
right: 25px;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button {
color: var(--muted-text-color);
position: absolute;
bottom: 13px;
right: 0;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button:hover, .save-attributes-button:hover {
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
}
.attribute-errors {
color: red;
padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */
}
</style>
<div class="attribute-list-editor" tabindex="200"></div>
<div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
<div class="bx bx-plus add-new-attribute-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
@ -84,61 +32,6 @@ const TPL = /*html*/`
</div>
`;
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (_item) => {
const item = _item as Suggestion;
const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
},
{
marker: "#",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `#${name}`,
name: name
};
});
},
minimumCharacters: 0
},
{
marker: "~",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `~${name}`,
name: name
};
});
},
minimumCharacters: 0
}
];
const editorConfig: EditorConfig = {
toolbar: {
items: []
},
placeholder: t("attribute_editor.placeholder"),
mention: {
feeds: mentionSetup
},
licenseKey: "GPL"
};
type AttributeCommandNames = FilteredCommandNames<CommandData>;
export default class AttributeEditorWidget extends NoteContextAwareWidget implements EventListener<"entitiesReloaded">, EventListener<"addNewLabel">, EventListener<"addNewRelation"> {
@ -324,7 +217,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
this.$editor.on("click", (e) => this.handleEditorClick(e));
this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig);
this.textEditor.model.document.on("change:data", () => this.dataChanged());
this.textEditor.editing.view.document.on(
"enter",

View File

@ -0,0 +1,10 @@
import AttributeEditor from "./components/AttributeEditor";
import { TabContext } from "./ribbon-interface";
export default function OwnedAttributesTab({ note }: TabContext) {
return (
<div className="attribute-list">
<AttributeEditor />
</div>
)
}

View File

@ -19,6 +19,7 @@ import FilePropertiesTab from "./FilePropertiesTab";
import ImagePropertiesTab from "./ImagePropertiesTab";
import NotePathsTab from "./NotePathsTab";
import NoteMapTab from "./NoteMapTab";
import OwnedAttributesTab from "./OwnedAttributesTab";
interface TitleContext {
note: FNote | null | undefined;
@ -105,9 +106,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
toggleCommand: "toggleRibbonTabBasicProperties"
},
{
// OwnedAttributeListWidget
title: t("owned_attribute_list.owned_attributes"),
icon: "bx bx-list-check"
icon: "bx bx-list-check",
content: OwnedAttributesTab,
show: ({note}) => !note?.isLaunchBarConfig(),
toggleCommand: "toggleRibbonTabOwnedAttributes"
},
{
// InheritedAttributesWidget

View File

@ -0,0 +1,78 @@
import { useEffect, useRef } from "preact/hooks"
import { AttributeEditor as CKEditor, EditorConfig, MentionFeed } from "@triliumnext/ckeditor5";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText) => note_autocomplete.autocompleteSourceForCKEditor(queryText),
itemRenderer: (_item) => {
const item = _item as Suggestion;
const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
},
{
marker: "#",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `#${name}`,
name: name
};
});
},
minimumCharacters: 0
},
{
marker: "~",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `~${name}`,
name: name
};
});
},
minimumCharacters: 0
}
];
const editorConfig: EditorConfig = {
toolbar: {
items: []
},
placeholder: t("attribute_editor.placeholder"),
mention: {
feeds: mentionSetup
},
licenseKey: "GPL"
};
export default function AttributeEditor() {
const editorContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!editorContainerRef.current) return;
CKEditor.create(editorContainerRef.current, editorConfig).then((textEditor) => {
});
}, []);
return (
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
<div ref={editorContainerRef} class="attribute-list-editor" tabindex={200} />
</div>
)
}

View File

@ -267,4 +267,68 @@
color: var(--muted-text-color);
display: none;
}
/* #endregion */
/* #region Attribute editor */
.attribute-list-editor {
border: 0 !important;
outline: 0 !important;
box-shadow: none !important;
padding: 0 0 0 5px !important;
margin: 0 !important;
max-height: 100px;
overflow: auto;
transition: opacity .1s linear;
}
.attribute-list-editor.ck-content .mention {
color: var(--muted-text-color) !important;
background: transparent !important;
}
.save-attributes-button {
color: var(--muted-text-color);
position: absolute;
bottom: 14px;
right: 25px;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button {
color: var(--muted-text-color);
position: absolute;
bottom: 13px;
right: 0;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button:hover, .save-attributes-button:hover {
border: 1px solid var(--button-border-color);
border-radius: var(--button-border-radius);
background: var(--button-background-color);
color: var(--button-text-color);
}
.attribute-errors {
color: red;
padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */
}
/* #endregion */
/* #region Owned Attributes */
.attribute-list {
margin-left: 7px;
margin-right: 7px;
margin-top: 5px;
margin-bottom: 2px;
position: relative;
}
.attribute-list-editor p {
margin: 0 !important;
}
/* #endregion */

View File

@ -5,40 +5,12 @@ import AttributeEditorWidget from "../attribute_widgets/attribute_editor.js";
import type { CommandListenerData } from "../../components/app_context.js";
import type FAttribute from "../../entities/fattribute.js";
const TPL = /*html*/`
<div class="attribute-list">
<style>
.attribute-list {
margin-left: 7px;
margin-right: 7px;
margin-top: 5px;
margin-bottom: 2px;
position: relative;
}
.attribute-list-editor p {
margin: 0 !important;
}
</style>
<div class="attr-editor-placeholder"></div>
</div>
`;
export default class OwnedAttributeListWidget extends NoteContextAwareWidget {
private attributeDetailWidget: AttributeDetailWidget;
private attributeEditorWidget: AttributeEditorWidget;
private $title!: JQuery<HTMLElement>;
get name() {
return "ownedAttributes";
}
get toggleCommand() {
return "toggleRibbonTabOwnedAttributes";
}
constructor() {
super();
@ -49,23 +21,6 @@ export default class OwnedAttributeListWidget extends NoteContextAwareWidget {
this.child(this.attributeEditorWidget, this.attributeDetailWidget);
}
getTitle() {
return {
show: !this.note?.isLaunchBarConfig(),
};
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$widget.find(".attr-editor-placeholder").replaceWith(this.attributeEditorWidget.render());
this.$widget.append(this.attributeDetailWidget.render());
this.$title = $("<div>");
}
async saveAttributesCommand() {
await this.attributeEditorWidget.save();
}