503 lines
15 KiB
JavaScript
Raw Normal View History

2024-07-29 10:14:27 +08:00
import { t } from "../../services/i18n.js";
2021-05-22 12:35:41 +02:00
import NoteContextAwareWidget from "../note_context_aware_widget.js";
2020-11-26 23:00:27 +01:00
import noteAutocompleteService from "../../services/note_autocomplete.js";
import server from "../../services/server.js";
2022-08-05 16:44:26 +02:00
import contextMenuService from "../../menus/context_menu.js";
2023-05-29 00:19:54 +02:00
import attributeParser from "../../services/attribute_parser.js";
2020-11-26 23:00:27 +01:00
import libraryLoader from "../../services/library_loader.js";
2021-04-16 23:01:56 +02:00
import froca from "../../services/froca.js";
2020-11-26 23:00:27 +01:00
import attributeRenderer from "../../services/attribute_renderer.js";
import noteCreateService from "../../services/note_create.js";
2021-08-25 22:49:24 +02:00
import attributeService from "../../services/attributes.js";
2023-05-29 00:19:54 +02:00
import linkService from "../../services/link.js";
2020-07-17 00:08:28 +02:00
const HELP_TEXT = `
2024-07-29 10:14:27 +08:00
<p>${t("attribute_editor.help_text_body1")}</p>
2024-07-29 10:14:27 +08:00
<p>${t("attribute_editor.help_text_body2")}</p>
2024-07-29 10:14:27 +08:00
<p>${t("attribute_editor.help_text_body3")}</p>`;
2020-07-17 00:08:28 +02:00
const TPL = `
2020-10-31 22:47:15 +01:00
<div style="position: relative; padding-top: 10px; padding-bottom: 10px">
2020-07-17 00:08:28 +02:00
<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;
2020-07-17 00:08:28 +02:00
overflow: auto;
transition: opacity .1s linear;
2020-07-17 00:08:28 +02:00
}
2021-03-29 23:29:14 +02:00
.attribute-list-editor.ck-content .mention {
color: var(--muted-text-color) !important;
background: transparent !important;
}
2020-07-17 00:08:28 +02:00
.save-attributes-button {
color: var(--muted-text-color);
position: absolute;
bottom: 14px;
2020-07-17 00:08:28 +02:00
right: 25px;
cursor: pointer;
border: 1px solid transparent;
font-size: 130%;
}
.add-new-attribute-button {
color: var(--muted-text-color);
position: absolute;
bottom: 13px;
2020-07-17 00:08:28 +02:00
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);
2020-07-17 00:08:28 +02:00
}
2020-07-17 23:55:59 +02:00
.attribute-errors {
color: red;
padding: 5px 50px 0px 5px; /* large right padding to avoid buttons */
2020-07-17 23:55:59 +02:00
}
2020-07-17 00:08:28 +02:00
</style>
<div class="attribute-list-editor" tabindex="200"></div>
2024-07-29 10:14:27 +08:00
<div class="bx bx-save save-attributes-button" title="${t("attribute_editor.save_attributes")}"></div>
<div class="bx bx-plus add-new-attribute-button" title="${t("attribute_editor.add_a_new_attribute")}"></div>
2020-07-17 23:55:59 +02:00
<div class="attribute-errors" style="display: none;"></div>
2020-07-17 00:08:28 +02:00
</div>
`;
const mentionSetup = {
feeds: [
{
marker: '@',
2020-09-21 22:08:54 +02:00
feed: queryText => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
2020-07-17 00:08:28 +02:00
itemRenderer: item => {
2020-08-12 23:39:05 +02:00
const itemElement = document.createElement('button');
2020-07-17 00:08:28 +02:00
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
},
{
marker: '#',
feed: async queryText => {
2023-04-14 16:49:06 +02:00
const names = await server.get(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
2020-07-17 00:08:28 +02:00
return names.map(name => {
return {
id: `#${name}`,
2020-07-17 00:08:28 +02:00
name: name
}
});
},
2023-06-29 11:44:28 +02:00
minimumCharacters: 0
2020-07-17 00:08:28 +02:00
},
{
marker: '~',
feed: async queryText => {
2023-04-14 16:49:06 +02:00
const names = await server.get(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
2020-07-17 00:08:28 +02:00
return names.map(name => {
return {
id: `~${name}`,
2020-07-17 00:08:28 +02:00
name: name
}
});
},
2023-06-29 11:44:28 +02:00
minimumCharacters: 0
2020-07-17 00:08:28 +02:00
}
]
};
const editorConfig = {
removePlugins: [
'Heading',
'Link',
'Autoformat',
'Bold',
'Italic',
'Underline',
'Strikethrough',
'Code',
'Superscript',
'Subscript',
'BlockQuote',
'Image',
'ImageCaption',
'ImageStyle',
'ImageToolbar',
'ImageUpload',
'ImageResize',
'List',
'TodoList',
'PasteFromOffice',
'Table',
'TableToolbar',
'TableProperties',
'TableCellProperties',
'Indent',
'IndentBlock',
'BlockToolbar',
'ParagraphButtonUI',
'HeadingButtonsUI',
'UploadimagePlugin',
'InternalLinkPlugin',
'MarkdownImportPlugin',
'CuttonotePlugin',
'TextTransformation',
'Font',
'FontColor',
'FontBackgroundColor',
'CodeBlock',
'SelectAll',
'IncludeNote',
2020-09-21 22:57:22 +02:00
'CutToNote',
'Mathematics',
'AutoformatMath',
2020-09-21 22:57:22 +02:00
'indentBlockShortcutPlugin',
'removeFormatLinksPlugin'
2020-07-17 00:08:28 +02:00
],
toolbar: {
items: []
},
placeholder: t("attribute_editor.placeholder"),
2020-07-17 00:08:28 +02:00
mention: mentionSetup
};
2021-05-22 12:35:41 +02:00
export default class AttributeEditorWidget extends NoteContextAwareWidget {
2020-07-17 23:55:59 +02:00
constructor(attributeDetailWidget) {
super();
this.attributeDetailWidget = attributeDetailWidget;
}
2020-07-17 00:08:28 +02:00
doRender() {
this.$widget = $(TPL);
this.$editor = this.$widget.find('.attribute-list-editor');
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);
2020-07-17 00:08:28 +02:00
}
this.attributeDetailWidget.hide();
});
2024-02-09 10:58:41 +01:00
this.$editor.on('blur', () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
2020-07-18 00:20:24 +02:00
2020-07-17 00:08:28 +02:00
this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button');
2020-07-17 23:55:59 +02:00
this.$addNewAttributeButton.on('click', e => this.addNewAttribute(e));
2020-07-17 00:08:28 +02:00
2020-07-17 23:55:59 +02:00
this.$saveAttributesButton = this.$widget.find('.save-attributes-button');
this.$saveAttributesButton.on('click', () => this.save());
2020-07-17 00:08:28 +02:00
2020-07-17 23:55:59 +02:00
this.$errors = this.$widget.find('.attribute-errors');
}
2020-07-17 00:08:28 +02:00
2020-07-17 23:55:59 +02:00
addNewAttribute(e) {
contextMenuService.show({
x: e.pageX,
y: e.pageY,
orientation: 'left',
items: [
2024-07-29 10:14:27 +08:00
{ title: t("attribute_editor.add_new_label"), command: "addNewLabel", uiIcon: "bx bx-hash" },
{ title: t("attribute_editor.add_new_relation"), command: "addNewRelation", uiIcon: "bx bx-transfer" },
{ title: "----" },
{ title: t("attribute_editor.add_new_label_definition"), command: "addNewLabelDefinition", uiIcon: "bx bx-empty" },
{ title: t("attribute_editor.add_new_relation_definition"), command: "addNewRelationDefinition", uiIcon: "bx bx-empty" },
2020-07-17 23:55:59 +02:00
],
2024-07-29 10:14:27 +08:00
selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command)
2020-07-17 23:55:59 +02:00
});
}
2020-07-17 00:08:28 +02:00
// triggered from keyboard shortcut
async addNewLabelEvent({ntxId}) {
2021-05-22 12:42:34 +02:00
if (this.isNoteContext(ntxId)) {
await this.refresh();
this.handleAddNewAttributeCommand('addNewLabel');
}
}
// triggered from keyboard shortcut
async addNewRelationEvent({ntxId}) {
2021-05-22 12:42:34 +02:00
if (this.isNoteContext(ntxId)) {
await this.refresh();
this.handleAddNewAttributeCommand('addNewRelation');
}
}
2020-07-17 23:55:59 +02:00
async handleAddNewAttributeCommand(command) {
const attrs = this.parseAttributes();
2020-07-17 00:08:28 +02:00
2020-07-17 23:55:59 +02:00
if (!attrs) {
return;
}
2020-07-17 00:08:28 +02:00
let type, name, value;
2020-07-17 23:55:59 +02:00
if (command === 'addNewLabel') {
type = 'label';
name = 'myLabel';
value = '';
2020-07-17 23:55:59 +02:00
} else if (command === 'addNewRelation') {
type = 'relation';
name = 'myRelation';
value = '';
2020-07-17 23:55:59 +02:00
} else if (command === 'addNewLabelDefinition') {
type = 'label';
name = 'label:myLabel';
value = 'promoted,single,text';
2020-07-17 23:55:59 +02:00
} else if (command === 'addNewRelationDefinition') {
type = 'label';
name = 'relation:myRelation';
value = 'promoted,single';
2020-07-17 23:55:59 +02:00
} else {
return;
}
2020-07-17 00:08:28 +02:00
2020-07-17 23:55:59 +02:00
attrs.push({
type,
name,
value,
2020-07-17 23:55:59 +02:00
isInheritable: false
2020-07-17 00:08:28 +02:00
});
2020-07-17 23:55:59 +02:00
2020-07-18 00:20:24 +02:00
await this.renderOwnedAttributes(attrs, false);
2020-07-17 23:55:59 +02:00
this.$editor.scrollTop(this.$editor[0].scrollHeight);
const rect = this.$editor[0].getBoundingClientRect();
setTimeout(() => {
// showing a little bit later because there's a conflict with outside click closing the attr detail
this.attributeDetailWidget.showAttributeDetail({
allAttributes: attrs,
attribute: attrs[attrs.length - 1],
isOwned: true,
x: (rect.left + rect.right) / 2,
y: rect.bottom,
focus: 'name'
2020-07-17 23:55:59 +02:00
});
}, 100);
2020-07-17 00:08:28 +02:00
}
async save() {
if (this.lastUpdatedNoteId !== this.noteId) {
// https://github.com/zadam/trilium/issues/3090
console.warn("Ignoring blur event because a different note is loaded.");
return;
}
2020-07-17 00:08:28 +02:00
const attributes = this.parseAttributes();
if (attributes) {
await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId);
this.$saveAttributesButton.fadeOut();
2023-06-30 11:18:34 +02:00
// 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);
2020-07-17 00:08:28 +02:00
}
}
parseAttributes() {
try {
2023-05-29 00:19:54 +02:00
return attributeParser.lexAndParse(this.getPreprocessedData());
2020-07-17 00:08:28 +02:00
}
catch (e) {
this.$errors.text(e.message).slideDown();
2020-07-17 00:08:28 +02:00
}
}
getPreprocessedData() {
const str = this.textEditor.getData()
2022-11-28 23:39:23 +01:00
.replace(/<a[^>]+href="(#[A-Za-z0-9_/]*)"[^>]*>[^<]*<\/a>/g, "$1")
.replace(/&nbsp;/g, " "); // otherwise .text() below outputs non-breaking space in unicode
return $("<div>").html(str).text();
}
2020-07-17 00:08:28 +02:00
async initEditor() {
}
dataChanged() {
this.lastUpdatedNoteId = this.noteId;
2020-07-17 23:55:59 +02:00
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
this.$errors.hide();
2020-07-17 23:55:59 +02:00
}
2020-07-17 00:08:28 +02:00
}
async handleEditorClick(e) {
2020-07-17 00:08:28 +02:00
const pos = this.textEditor.model.document.selection.getFirstPosition();
if (pos && pos.textNode && pos.textNode.data) {
2020-07-17 00:08:28 +02:00
const clickIndex = this.getClickIndex(pos);
2020-07-17 23:55:59 +02:00
let parsedAttrs;
try {
2023-05-29 00:19:54 +02:00
parsedAttrs = attributeParser.lexAndParse(this.getPreprocessedData(), true);
2020-07-17 23:55:59 +02:00
}
catch (e) {
2023-06-30 11:18:34 +02:00
// the input is incorrect because the user messed up with it and now needs to fix it manually
2020-07-17 23:55:59 +02:00
return null;
}
2020-07-17 00:08:28 +02:00
let matchedAttr = null;
for (const attr of parsedAttrs) {
if (clickIndex > attr.startIndex && clickIndex <= attr.endIndex) {
2020-07-17 00:08:28 +02:00
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);
2020-07-17 00:08:28 +02:00
}
else {
this.showHelpTooltip();
}
}
2020-08-28 22:52:57 +02:00
showHelpTooltip() {
this.attributeDetailWidget.hide();
this.$editor.tooltip({
trigger: 'focus',
html: true,
title: HELP_TEXT,
placement: 'bottom',
offset: "0,30"
});
this.$editor.tooltip('show');
2020-07-17 00:08:28 +02:00
}
getClickIndex(pos) {
let clickIndex = pos.offset - pos.textNode.startOffset;
let curNode = pos.textNode;
while (curNode.previousSibling) {
curNode = curNode.previousSibling;
if (curNode.name === 'reference') {
clickIndex += curNode._attrs.get('notePath').length + 1;
} else {
clickIndex += curNode.data.length;
}
}
return clickIndex;
}
2023-06-29 12:19:01 +02:00
async loadReferenceLinkTitle($el, href) {
const {noteId} = linkService.parseNavigationStateFromUrl(href);
2021-04-16 22:57:37 +02:00
const note = await froca.getNote(noteId, true);
2020-07-17 00:08:28 +02:00
const title = note ? note.title : '[missing]';
2020-07-17 00:08:28 +02:00
$el.text(title);
}
async refreshWithNote(note) {
2020-07-18 00:20:24 +02:00
await this.renderOwnedAttributes(note.getOwnedAttributes(), true);
2020-07-17 00:08:28 +02:00
}
2020-07-18 00:20:24 +02:00
async renderOwnedAttributes(ownedAttributes, saved) {
2023-06-29 12:19:01 +02:00
// attrs are not resorted if position changes after the initial load
2023-11-03 01:11:47 +01:00
ownedAttributes.sort((a, b) => a.position - b.position);
let htmlAttrs = (await attributeRenderer.renderAttributes(ownedAttributes, true)).html();
if (htmlAttrs.length > 0) {
htmlAttrs += "&nbsp;";
}
this.textEditor.setData(htmlAttrs);
2020-07-17 23:55:59 +02:00
2020-07-18 00:20:24 +02:00
if (saved) {
this.lastSavedContent = this.textEditor.getData();
2020-07-17 23:55:59 +02:00
2020-07-18 00:20:24 +02:00
this.$saveAttributesButton.fadeOut(0);
}
2020-07-17 00:08:28 +02:00
}
2020-09-21 22:08:54 +02:00
async createNoteForReferenceLink(title) {
const {note} = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
2020-09-21 22:08:54 +02:00
activate: false,
title: title
});
2023-04-15 00:06:13 +02:00
return note.getBestNotePathString();
2020-09-21 22:08:54 +02:00
}
2020-09-21 23:08:39 +02:00
async updateAttributeList(attributes) {
await this.renderOwnedAttributes(attributes, false);
2020-07-17 00:08:28 +02:00
}
focus() {
this.$editor.trigger('focus');
this.textEditor.model.change( writer => {
const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), 'end');
writer.setSelection(positionAt);
} );
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributeRows(this.componentId).find(attr => attributeService.isAffecting(attr, this.note))) {
this.refresh();
}
}
2020-07-17 00:08:28 +02:00
}