import server from "../../services/server.js"; import froca from "../../services/froca.js"; import treeService from "../../services/tree.js"; import linkService from "../../services/link.js"; import attributeAutocompleteService from "../../services/attribute_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; import promotedAttributeDefinitionParser from '../../services/promoted_attribute_definition_parser.js'; import NoteContextAwareWidget from "../note_context_aware_widget.js"; import SpacedUpdate from "../../services/spaced_update.js"; import utils from "../../services/utils.js"; const TPL = `
Name: | |
---|---|
Value: | |
Target note: |
|
Promoted: | |
Multiplicity: | |
Type: | |
Precision: |
digits
|
Inverse relation: |
|
Inheritable: |
runAtHour
to specify at which hour.#run=hourly
. Can be defined multiple times for more runs during the day.",
"disableInclusion": "scripts with this label won't be included into parent script execution.",
"sorted": "keeps child notes sorted by title alphabetically",
"hidePromotedAttributes": "Hide promoted attributes on this note",
"readOnly": "editor is in read only mode. Works only for text and code notes.",
"autoReadOnlyDisabled": "text/code notes can be set automatically into read mode when they are too large. You can disable this behavior on per-note basis by adding this label to the note",
"appCss": "marks CSS notes which are loaded into the Trilium application and can thus be used to modify Trilium's looks.",
"appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.",
"cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.",
"iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.",
"pageSize": "number of items per page in note listing",
"customRequestHandler": 'see Custom request handler',
"customResourceProvider": 'see Custom request handler',
"widget": "marks this note as a custom widget which will be added to the Trilium component tree",
"workspace": "marks this note as a workspace which allows easy hoisting",
"workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note",
"workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note",
"searchHome": "new search notes will be created as children of this note",
"hoistedSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this note",
"inbox": "default inbox location for new notes",
"hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note",
"sqlConsoleHome": "default location of SQL console notes",
},
"relation": {
"runOnNoteCreation": "executes when note is created on backend",
"runOnNoteTitleChange": "executes when note title is changed (includes note creation as well)",
"runOnNoteChange": "executes when note is changed (includes note creation as well)",
"runOnChildNoteCreation": "executes when new note is created under this note",
"runOnAttributeCreation": "executes when new attribute is created under this note",
"runOnAttributeChange": "executes when attribute is changed under this note",
"template": "attached note's attributes will be inherited even without parent-child relationship. See template for details.",
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
"widget": "target of this relation will be executed and rendered as a widget in the sidebar"
}
};
export default class AttributeDetailWidget extends NoteContextAwareWidget {
async refresh() {
// switching note/tab should close the widget
this.hide();
}
doRender() {
this.relatedNotesSpacedUpdate = new SpacedUpdate(async () => this.updateRelatedNotes(), 1000);
this.$widget = $(TPL);
utils.bindElShortcut(this.$widget, 'ctrl+return', () => this.saveAndClose());
utils.bindElShortcut(this.$widget, 'esc', () => this.cancelAndClose());
this.$title = this.$widget.find('.attr-detail-title');
this.$inputName = this.$widget.find('.attr-input-name');
this.$inputName.on('keyup', () => this.userEditedAttribute());
this.$inputName.on('change', () => this.userEditedAttribute());
this.$inputName.on('autocomplete:closed', () => this.userEditedAttribute());
this.$inputName.on('focus', () => {
attributeAutocompleteService.initAttributeNameAutocomplete({
$el: this.$inputName,
attributeType: () => ['relation', 'relation-definition'].includes(this.attrType) ? 'relation' : 'label',
open: true
});
});
this.$rowValue = this.$widget.find('.attr-row-value');
this.$inputValue = this.$widget.find('.attr-input-value');
this.$inputValue.on('keyup', () => this.userEditedAttribute());
this.$inputValue.on('change', () => this.userEditedAttribute());
this.$inputValue.on('autocomplete:closed', () => this.userEditedAttribute());
this.$inputValue.on('focus', () => {
attributeAutocompleteService.initLabelValueAutocomplete({
$el: this.$inputValue,
open: true,
nameCallback: () => this.$inputName.val()
});
});
this.$rowPromoted = this.$widget.find('.attr-row-promoted');
this.$inputPromoted = this.$widget.find('.attr-input-promoted');
this.$inputPromoted.on('change', () => this.userEditedAttribute());
this.$rowMultiplicity = this.$widget.find('.attr-row-multiplicity');
this.$inputMultiplicity = this.$widget.find('.attr-input-multiplicity');
this.$inputMultiplicity.on('change', () => this.userEditedAttribute());
this.$rowLabelType = this.$widget.find('.attr-row-label-type');
this.$inputLabelType = this.$widget.find('.attr-input-label-type');
this.$inputLabelType.on('change', () => this.userEditedAttribute());
this.$rowNumberPrecision = this.$widget.find('.attr-row-number-precision');
this.$inputNumberPrecision = this.$widget.find('.attr-input-number-precision');
this.$inputNumberPrecision.on('change', () => this.userEditedAttribute());
this.$rowInverseRelation = this.$widget.find('.attr-row-inverse-relation');
this.$inputInverseRelation = this.$widget.find('.attr-input-inverse-relation');
this.$inputInverseRelation.on('keyup', () => this.userEditedAttribute());
this.$rowTargetNote = this.$widget.find('.attr-row-target-note');
this.$inputTargetNote = this.$widget.find('.attr-input-target-note');
noteAutocompleteService.initNoteAutocomplete(this.$inputTargetNote, {allowCreatingNotes: true})
.on('autocomplete:noteselected', (event, suggestion, dataset) => {
if (!suggestion.notePath) {
return false;
}
const pathChunks = suggestion.notePath.split('/');
this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId
this.triggerCommand('updateAttributeList', { attributes: this.allAttributes });
this.updateRelatedNotes();
});
this.$inputInheritable = this.$widget.find('.attr-input-inheritable');
this.$inputInheritable.on('change', () => this.userEditedAttribute());
this.$closeAttrDetailButton = this.$widget.find('.close-attr-detail-button');
this.$closeAttrDetailButton.on('click', () => this.cancelAndClose());
this.$attrIsOwnedBy = this.$widget.find('.attr-is-owned-by');
this.$attrSaveDeleteButtonContainer = this.$widget.find('.attr-save-delete-button-container');
this.$saveAndCloseButton = this.$widget.find('.attr-save-changes-and-close-button');
this.$saveAndCloseButton.on('click', () => this.saveAndClose());
this.$deleteButton = this.$widget.find('.attr-delete-button');
this.$deleteButton.on('click', async () => {
await this.triggerCommand('updateAttributeList', {
attributes: this.allAttributes.filter(attr => attr !== this.attribute)
});
await this.triggerCommand('saveAttributes');
this.hide();
});
this.$attrHelp = this.$widget.find('.attr-help');
this.$relatedNotesContainer = this.$widget.find('.related-notes-container');
this.$relatedNotesTitle = this.$relatedNotesContainer.find('.related-notes-tile');
this.$relatedNotesList = this.$relatedNotesContainer.find('.related-notes-list');
this.$relatedNotesMoreNotes = this.$relatedNotesContainer.find('.related-notes-more-notes');
$(window).on('mouseup', e => {
if (!$(e.target).closest(this.$widget[0]).length
&& !$(e.target).closest(".algolia-autocomplete").length
&& !$(e.target).closest("#context-menu-container").length) {
this.hide();
}
});
}
async showAttributeDetail({allAttributes, attribute, isOwned, x, y, focus}) {
if (!attribute) {
this.hide();
return;
}
utils.saveFocusedElement();
this.attrType = this.getAttrType(attribute);
const attrName =
this.attrType === 'label-definition' ? attribute.name.substr(6)
: (this.attrType === 'relation-definition' ? attribute.name.substr(9) : attribute.name);
const definition = this.attrType.endsWith('-definition')
? promotedAttributeDefinitionParser.parse(attribute.value)
: {};
this.$title.text(ATTR_TITLES[this.attrType]);
this.allAttributes = allAttributes;
this.attribute = attribute;
// can be slightly slower so just make it async
this.updateRelatedNotes();
this.$attrSaveDeleteButtonContainer.toggle(!!isOwned);
if (isOwned) {
this.$attrIsOwnedBy.hide();
}
else {
this.$attrIsOwnedBy
.show()
.empty()
.append(attribute.type === 'label' ? 'Label' : 'Relation')
.append(' is owned by note ')
.append(await linkService.createNoteLink(attribute.noteId))
}
this.$inputName
.val(attrName)
.attr('readonly', () => !isOwned);
this.$rowValue.toggle(this.attrType === 'label');
this.$rowTargetNote.toggle(this.attrType === 'relation');
this.$rowPromoted.toggle(['label-definition', 'relation-definition'].includes(this.attrType));
this.$inputPromoted
.prop("checked", !!definition.isPromoted)
.attr('disabled', () => !isOwned);
this.$rowMultiplicity.toggle(['label-definition', 'relation-definition'].includes(this.attrType));
this.$inputMultiplicity
.val(definition.multiplicity)
.attr('disabled', () => !isOwned);
this.$rowLabelType.toggle(this.attrType === 'label-definition');
this.$inputLabelType
.val(definition.labelType)
.attr('disabled', () => !isOwned);
this.$rowNumberPrecision.toggle(this.attrType === 'label-definition' && definition.labelType === 'number');
this.$inputNumberPrecision
.val(definition.numberPrecision)
.attr('disabled', () => !isOwned);
this.$rowInverseRelation.toggle(this.attrType === 'relation-definition');
this.$inputInverseRelation
.val(definition.inverseRelation)
.attr('disabled', () => !isOwned);
if (attribute.type === 'label') {
this.$inputValue
.val(attribute.value)
.attr('readonly', () => !isOwned);
}
else if (attribute.type === 'relation') {
this.$inputTargetNote
.attr('readonly', () => !isOwned)
.val("")
.setSelectedNotePath("");
if (attribute.value) {
const targetNote = await froca.getNote(attribute.value);
if (targetNote) {
this.$inputTargetNote
.val(targetNote ? targetNote.title : "")
.setSelectedNotePath(attribute.value);
}
}
}
this.$inputInheritable
.prop("checked", !!attribute.isInheritable)
.attr('disabled', () => !isOwned);
this.updateHelp();
this.toggleInt(true);
const offset = this.parent.$widget.offset();
const detPosition = this.getDetailPosition(x, offset);
this.$widget
.css("left", detPosition.left)
.css("right", detPosition.right)
.css("top", y - offset.top + 70)
.css("max-height",
this.$widget.outerHeight() + y > $(window).height() - 50
? $(window).height() - y - 50
: 10000);
if (focus === 'name') {
this.$inputName
.trigger('focus')
.trigger('select');
}
}
getDetailPosition(x, offset) {
let left = x - offset.left - this.$widget.outerWidth() / 2;
let right = "";
if (left < 0) {
left = 10;
} else {
const rightEdge = left + this.$widget.outerWidth();
if (rightEdge > this.parent.$widget.outerWidth() - 10) {
left = "";
right = 10;
}
}
return {left, right};
}
async saveAndClose() {
await this.triggerCommand('saveAttributes');
this.hide();
utils.focusSavedElement();
}
async cancelAndClose() {
await this.triggerCommand('reloadAttributes');
this.hide();
utils.focusSavedElement();
}
userEditedAttribute() {
this.updateAttributeInEditor();
this.updateHelp();
this.relatedNotesSpacedUpdate.scheduleUpdate();
}
updateHelp() {
const attrName = this.$inputName.val();
if (this.attrType in ATTR_HELP && attrName in ATTR_HELP[this.attrType]) {
this.$attrHelp
.empty()
.append($("