mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 09:58:32 +02:00
split out attribute_editor widget, WIP
This commit is contained in:
parent
68d8b1c8a9
commit
9f527f0330
60
src/public/app/services/attribute_renderer.js
Normal file
60
src/public/app/services/attribute_renderer.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import ws from "./ws.js";
|
||||||
|
|
||||||
|
function renderAttribute(attribute, $container, renderIsInheritable) {
|
||||||
|
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : '';
|
||||||
|
|
||||||
|
if (attribute.type === 'label') {
|
||||||
|
$container.append(document.createTextNode('#' + attribute.name + isInheritable));
|
||||||
|
|
||||||
|
if (attribute.value) {
|
||||||
|
$container.append('=');
|
||||||
|
$container.append(document.createTextNode(formatValue(attribute.value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$container.append(' ');
|
||||||
|
} else if (attribute.type === 'relation') {
|
||||||
|
if (attribute.isAutoLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute.value) {
|
||||||
|
$container.append(document.createTextNode('~' + attribute.name + isInheritable + "="));
|
||||||
|
$container.append(createNoteLink(attribute.value));
|
||||||
|
$container.append(" ");
|
||||||
|
} else {
|
||||||
|
ws.logError(`Relation ${attribute.attributeId} has empty target`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ws.logError("Unknown attr type: " + attribute.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatValue(val) {
|
||||||
|
if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
else if (!val.includes('"')) {
|
||||||
|
return '"' + val + '"';
|
||||||
|
}
|
||||||
|
else if (!val.includes("'")) {
|
||||||
|
return "'" + val + "'";
|
||||||
|
}
|
||||||
|
else if (!val.includes("`")) {
|
||||||
|
return "`" + val + "`";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return '"' + val.replace(/"/g, '\\"') + '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNoteLink(noteId) {
|
||||||
|
return $("<a>", {
|
||||||
|
href: '#' + noteId,
|
||||||
|
class: 'reference-link',
|
||||||
|
'data-note-path': noteId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
renderAttribute
|
||||||
|
}
|
392
src/public/app/widgets/attribute_editor.js
Normal file
392
src/public/app/widgets/attribute_editor.js
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
import TabAwareWidget from "./tab_aware_widget.js";
|
||||||
|
import noteAutocompleteService from "../services/note_autocomplete.js";
|
||||||
|
import server from "../services/server.js";
|
||||||
|
import contextMenuService from "../services/context_menu.js";
|
||||||
|
import attributesParser from "../services/attribute_parser.js";
|
||||||
|
import libraryLoader from "../services/library_loader.js";
|
||||||
|
import treeCache from "../services/tree_cache.js";
|
||||||
|
import attributeRenderer from "../services/attribute_renderer.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<div style="position: relative">
|
||||||
|
<style>
|
||||||
|
.attribute-list-editor {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 0 0 5px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-attributes-button {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
right: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-new-attribute-button {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 130%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-new-attribute-button:hover, .save-attributes-button:hover {
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="attribute-list-editor" tabindex="200"></div>
|
||||||
|
|
||||||
|
<div class="bx bx-save save-attributes-button" title="Save attributes <enter>, <tab>)"></div>
|
||||||
|
|
||||||
|
<div class="bx bx-plus add-new-attribute-button" title="Add a new attribute"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mentionSetup = {
|
||||||
|
feeds: [
|
||||||
|
{
|
||||||
|
marker: '@',
|
||||||
|
feed: queryText => {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
noteAutocompleteService.autocompleteSource(queryText, rows => {
|
||||||
|
res(rows.map(row => {
|
||||||
|
return {
|
||||||
|
id: '@' + row.notePathTitle,
|
||||||
|
name: row.notePathTitle,
|
||||||
|
link: '#' + row.notePath,
|
||||||
|
notePath: row.notePath,
|
||||||
|
highlightedNotePathTitle: row.highlightedNotePathTitle
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
itemRenderer: item => {
|
||||||
|
const itemElement = document.createElement('span');
|
||||||
|
|
||||||
|
itemElement.classList.add('mentions-item');
|
||||||
|
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
|
||||||
|
|
||||||
|
return itemElement;
|
||||||
|
},
|
||||||
|
minimumCharacters: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
marker: '#',
|
||||||
|
feed: async queryText => {
|
||||||
|
const names = await server.get(`attributes/names/?type=label&query=${encodeURIComponent(queryText)}`);
|
||||||
|
|
||||||
|
return names.map(name => {
|
||||||
|
return {
|
||||||
|
id: '#' + name,
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
minimumCharacters: 0,
|
||||||
|
attributeMention: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
marker: '~',
|
||||||
|
feed: async queryText => {
|
||||||
|
const names = await server.get(`attributes/names/?type=relation&query=${encodeURIComponent(queryText)}`);
|
||||||
|
|
||||||
|
return names.map(name => {
|
||||||
|
return {
|
||||||
|
id: '~' + name,
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
minimumCharacters: 0,
|
||||||
|
attributeMention: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
removePlugins: [
|
||||||
|
'Enter',
|
||||||
|
'ShiftEnter',
|
||||||
|
'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',
|
||||||
|
'CutToNote'
|
||||||
|
],
|
||||||
|
toolbar: {
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
placeholder: "Type the labels and relations here ...",
|
||||||
|
mention: mentionSetup
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class AttributeEditorWidget extends TabAwareWidget {
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$editor = this.$widget.find('.attribute-list-editor');
|
||||||
|
|
||||||
|
this.initialized = this.initEditor();
|
||||||
|
|
||||||
|
this.$editor.on('keydown', async e => {
|
||||||
|
const keycode = (e.keyCode ? e.keyCode : e.which);
|
||||||
|
|
||||||
|
if (keycode === 13) {
|
||||||
|
this.triggerCommand('focusOnDetail', {tabId: this.tabContext.tabId});
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attributeDetailWidget.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button');
|
||||||
|
this.$addNewAttributeButton.on('click', e => {
|
||||||
|
contextMenuService.show({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
orientation: 'left',
|
||||||
|
items: [
|
||||||
|
{title: "Add new label", command: "addNewLabel", uiIcon: "hash"},
|
||||||
|
{title: "Add new relation", command: "addNewRelation", uiIcon: "transfer"},
|
||||||
|
{title: "----"},
|
||||||
|
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "empty"},
|
||||||
|
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "empty"},
|
||||||
|
],
|
||||||
|
selectMenuItemHandler: async ({command}) => {
|
||||||
|
const attrs = this.parseAttributes();
|
||||||
|
|
||||||
|
if (!attrs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type, name;
|
||||||
|
|
||||||
|
if (command === 'addNewLabel') {
|
||||||
|
type = 'label';
|
||||||
|
name = 'fillName';
|
||||||
|
}
|
||||||
|
else if (command === 'addNewRelation') {
|
||||||
|
type = 'relation';
|
||||||
|
name = 'fillName';
|
||||||
|
}
|
||||||
|
else if (command === 'addNewLabelDefinition') {
|
||||||
|
type = 'label';
|
||||||
|
name = 'label:fillName';
|
||||||
|
}
|
||||||
|
else if (command === 'addNewRelationDefinition') {
|
||||||
|
type = 'label';
|
||||||
|
name = 'relation:fillName';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs.push({
|
||||||
|
type,
|
||||||
|
name,
|
||||||
|
value: '',
|
||||||
|
isInheritable: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.renderOwnedAttributes(attrs);
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const attributes = this.parseAttributes();
|
||||||
|
|
||||||
|
if (attributes) {
|
||||||
|
await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseAttributes() {
|
||||||
|
try {
|
||||||
|
const attrs = attributesParser.lexAndParse(this.textEditor.getData());
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this.$widget.attr("title", e.message);
|
||||||
|
this.$widget.addClass("error");
|
||||||
|
|
||||||
|
this.$ownedExpander.addClass("error");
|
||||||
|
this.$ownedExpanderText.text(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initEditor() {
|
||||||
|
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||||
|
|
||||||
|
this.$widget.show();
|
||||||
|
|
||||||
|
this.$editor.on("click", e => this.handleEditorClick(e));
|
||||||
|
|
||||||
|
this.textEditor = await BalloonEditor.create(this.$editor[0], editorConfig);
|
||||||
|
this.textEditor.model.document.on('change:data', () => this.dataChanged());
|
||||||
|
|
||||||
|
// disable spellcheck for attribute editor
|
||||||
|
this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot()));
|
||||||
|
|
||||||
|
//await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector.js');
|
||||||
|
//CKEditorInspector.attach(this.textEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataChanged() {
|
||||||
|
console.log("Data changed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleEditorClick(e) {
|
||||||
|
const pos = this.textEditor.model.document.selection.getFirstPosition();
|
||||||
|
|
||||||
|
if (pos && pos.textNode && pos.textNode.data) {
|
||||||
|
const clickIndex = this.getClickIndex(pos);
|
||||||
|
|
||||||
|
const parsedAttrs = attributesParser.lexAndParse(this.textEditor.getData(), true);
|
||||||
|
|
||||||
|
let matchedAttr = null;
|
||||||
|
|
||||||
|
for (const attr of parsedAttrs) {
|
||||||
|
if (clickIndex >= attr.startIndex && clickIndex <= attr.endIndex) {
|
||||||
|
matchedAttr = attr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.attributeDetailWidget.showAttributeDetail({
|
||||||
|
allAttributes: parsedAttrs,
|
||||||
|
attribute: matchedAttr,
|
||||||
|
isOwned: true,
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadReferenceLinkTitle(noteId, $el) {
|
||||||
|
const note = await treeCache.getNote(noteId, true);
|
||||||
|
|
||||||
|
let title;
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
title = '[missing]';
|
||||||
|
}
|
||||||
|
else if (!note.isDeleted) {
|
||||||
|
title = note.title;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
title = note.isErased ? '[erased]' : `${note.title} (deleted)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
$el.text(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshWithNote(note) {
|
||||||
|
await this.renderOwnedAttributes(note.getOwnedAttributes());
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderOwnedAttributes(ownedAttributes) {
|
||||||
|
const $attributesContainer = $("<div>");
|
||||||
|
|
||||||
|
for (const attribute of ownedAttributes) {
|
||||||
|
attributeRenderer.renderAttribute(attribute, $attributesContainer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.textEditor.setData($attributesContainer.html());
|
||||||
|
}
|
||||||
|
|
||||||
|
async focusOnAttributesEvent({tabId}) {
|
||||||
|
if (this.tabContext.tabId === tabId) {
|
||||||
|
this.$editor.trigger('focus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAttributeListCommand({attributes}) {
|
||||||
|
this.renderOwnedAttributes(attributes);
|
||||||
|
}
|
||||||
|
}
|
@ -1,128 +1,7 @@
|
|||||||
import TabAwareWidget from "./tab_aware_widget.js";
|
import TabAwareWidget from "./tab_aware_widget.js";
|
||||||
import libraryLoader from "../services/library_loader.js";
|
|
||||||
import noteAutocompleteService from "../services/note_autocomplete.js";
|
|
||||||
import treeCache from "../services/tree_cache.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import ws from "../services/ws.js";
|
|
||||||
import SpacedUpdate from "../services/spaced_update.js";
|
|
||||||
import attributesParser from "../services/attribute_parser.js";
|
|
||||||
import AttributeDetailWidget from "./attribute_detail.js";
|
import AttributeDetailWidget from "./attribute_detail.js";
|
||||||
import contextMenuService from "../services/context_menu.js";
|
import attributeRenderer from "../services/attribute_renderer.js";
|
||||||
|
import AttributeEditorWidget from "./attribute_editor.js";
|
||||||
const mentionSetup = {
|
|
||||||
feeds: [
|
|
||||||
{
|
|
||||||
marker: '@',
|
|
||||||
feed: queryText => {
|
|
||||||
return new Promise((res, rej) => {
|
|
||||||
noteAutocompleteService.autocompleteSource(queryText, rows => {
|
|
||||||
res(rows.map(row => {
|
|
||||||
return {
|
|
||||||
id: '@' + row.notePathTitle,
|
|
||||||
name: row.notePathTitle,
|
|
||||||
link: '#' + row.notePath,
|
|
||||||
notePath: row.notePath,
|
|
||||||
highlightedNotePathTitle: row.highlightedNotePathTitle
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
itemRenderer: item => {
|
|
||||||
const itemElement = document.createElement('span');
|
|
||||||
|
|
||||||
itemElement.classList.add('mentions-item');
|
|
||||||
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
|
|
||||||
|
|
||||||
return itemElement;
|
|
||||||
},
|
|
||||||
minimumCharacters: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: '#',
|
|
||||||
feed: async queryText => {
|
|
||||||
const names = await server.get(`attributes/names/?type=label&query=${encodeURIComponent(queryText)}`);
|
|
||||||
|
|
||||||
return names.map(name => {
|
|
||||||
return {
|
|
||||||
id: '#' + name,
|
|
||||||
name: name
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
minimumCharacters: 0,
|
|
||||||
attributeMention: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
marker: '~',
|
|
||||||
feed: async queryText => {
|
|
||||||
const names = await server.get(`attributes/names/?type=relation&query=${encodeURIComponent(queryText)}`);
|
|
||||||
|
|
||||||
return names.map(name => {
|
|
||||||
return {
|
|
||||||
id: '~' + name,
|
|
||||||
name: name
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
minimumCharacters: 0,
|
|
||||||
attributeMention: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const editorConfig = {
|
|
||||||
removePlugins: [
|
|
||||||
'Enter',
|
|
||||||
'ShiftEnter',
|
|
||||||
'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',
|
|
||||||
'CutToNote'
|
|
||||||
],
|
|
||||||
toolbar: {
|
|
||||||
items: []
|
|
||||||
},
|
|
||||||
placeholder: "Type the labels and relations here ...",
|
|
||||||
mention: mentionSetup
|
|
||||||
};
|
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="attribute-list">
|
<div class="attribute-list">
|
||||||
@ -132,17 +11,6 @@ const TPL = `
|
|||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attribute-list-editor {
|
|
||||||
border: 0 !important;
|
|
||||||
outline: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
padding: 0 0 0 5px !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
max-height: 200px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inherited-attributes {
|
.inherited-attributes {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
@ -192,39 +60,6 @@ const TPL = `
|
|||||||
.attr-expander:not(.error):hover .attr-expander-text {
|
.attr-expander:not(.error):hover .attr-expander-text {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attr-expander.error .attr-expander-text {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attr-expander.error hr {
|
|
||||||
border-color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-attributes-button {
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 3px;
|
|
||||||
right: 25px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-size: 130%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-new-attribute-button {
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 3px;
|
|
||||||
right: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-size: 130%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-new-attribute-button:hover, .save-attributes-button:hover {
|
|
||||||
border: 1px solid var(--main-border-color);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="attr-expander attr-owned-expander">
|
<div class="attr-expander attr-owned-expander">
|
||||||
@ -236,13 +71,7 @@ const TPL = `
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="attr-display">
|
<div class="attr-display">
|
||||||
<div style="position: relative">
|
<div class="attr-editor-placeholder"></div>
|
||||||
<div class="attribute-list-editor" tabindex="200"></div>
|
|
||||||
|
|
||||||
<div class="bx bx-save save-attributes-button" title="Save attributes <enter>, <tab>)"></div>
|
|
||||||
|
|
||||||
<div class="bx bx-plus add-new-attribute-button" title="Add a new attribute"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="w-100 attr-inherited-empty-expander" style="margin-bottom: 10px;">
|
<hr class="w-100 attr-inherited-empty-expander" style="margin-bottom: 10px;">
|
||||||
|
|
||||||
@ -264,20 +93,14 @@ export default class AttributeListWidget extends TabAwareWidget {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.attributeEditorWidget = new AttributeEditorWidget().setParent(this);
|
||||||
this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
|
this.attributeDetailWidget = new AttributeDetailWidget().setParent(this);
|
||||||
|
|
||||||
this.spacedUpdate = new SpacedUpdate(() => {
|
this.child(this.attributeEditorWidget, this.attributeDetailWidget);
|
||||||
this.parseAttributes();
|
|
||||||
|
|
||||||
this.attributeDetailWidget.hide();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.$editor = this.$widget.find('.attribute-list-editor');
|
|
||||||
|
|
||||||
this.initialized = this.initEditor();
|
|
||||||
|
|
||||||
this.$attrDisplay = this.$widget.find('.attr-display');
|
this.$attrDisplay = this.$widget.find('.attr-display');
|
||||||
|
|
||||||
@ -309,201 +132,14 @@ export default class AttributeListWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
this.$inheritedEmptyExpander = this.$widget.find('.attr-inherited-empty-expander');
|
this.$inheritedEmptyExpander = this.$widget.find('.attr-inherited-empty-expander');
|
||||||
|
|
||||||
this.$editor.on('keydown', async e => {
|
this.$widget.find('.attr-editor-placeholder').replaceWith(this.attributeEditorWidget.render());
|
||||||
const keycode = (e.keyCode ? e.keyCode : e.which);
|
|
||||||
|
|
||||||
if (keycode === 13) {
|
|
||||||
this.triggerCommand('focusOnDetail', {tabId: this.tabContext.tabId});
|
|
||||||
|
|
||||||
await this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attributeDetailWidget.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button');
|
|
||||||
this.$addNewAttributeButton.on('click', e => {
|
|
||||||
contextMenuService.show({
|
|
||||||
x: e.pageX,
|
|
||||||
y: e.pageY,
|
|
||||||
orientation: 'left',
|
|
||||||
items: [
|
|
||||||
{title: "Add new label", command: "addNewLabel", uiIcon: "hash"},
|
|
||||||
{title: "Add new relation", command: "addNewRelation", uiIcon: "transfer"},
|
|
||||||
{title: "----"},
|
|
||||||
{title: "Add new label definition", command: "addNewLabelDefinition", uiIcon: "empty"},
|
|
||||||
{title: "Add new relation definition", command: "addNewRelationDefinition", uiIcon: "empty"},
|
|
||||||
],
|
|
||||||
selectMenuItemHandler: async ({command}) => {
|
|
||||||
const attrs = this.parseAttributes();
|
|
||||||
|
|
||||||
if (!attrs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let type, name;
|
|
||||||
|
|
||||||
if (command === 'addNewLabel') {
|
|
||||||
type = 'label';
|
|
||||||
name = 'fillName';
|
|
||||||
}
|
|
||||||
else if (command === 'addNewRelation') {
|
|
||||||
type = 'relation';
|
|
||||||
name = 'fillName';
|
|
||||||
}
|
|
||||||
else if (command === 'addNewLabelDefinition') {
|
|
||||||
type = 'label';
|
|
||||||
name = 'label:fillName';
|
|
||||||
}
|
|
||||||
else if (command === 'addNewRelationDefinition') {
|
|
||||||
type = 'label';
|
|
||||||
name = 'relation:fillName';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs.push({
|
|
||||||
type,
|
|
||||||
name,
|
|
||||||
value: '',
|
|
||||||
isInheritable: false
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.renderOwnedAttributes(attrs);
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$widget.append(this.attributeDetailWidget.render());
|
this.$widget.append(this.attributeDetailWidget.render());
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
|
||||||
const attributes = this.parseAttributes();
|
|
||||||
|
|
||||||
if (attributes) {
|
|
||||||
await server.put(`notes/${this.noteId}/attributes`, attributes, this.componentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseAttributes() {
|
|
||||||
try {
|
|
||||||
const attrs = attributesParser.lexAndParse(this.textEditor.getData());
|
|
||||||
|
|
||||||
this.$widget.removeClass("error");
|
|
||||||
this.$widget.removeAttr("title");
|
|
||||||
|
|
||||||
this.$ownedExpander.removeClass("error");
|
|
||||||
this.$ownedExpanderText.text(attrs.length + ' owned ' + this.attrPlural(attrs.length));
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.$widget.attr("title", e.message);
|
|
||||||
this.$widget.addClass("error");
|
|
||||||
|
|
||||||
this.$ownedExpander.addClass("error");
|
|
||||||
this.$ownedExpanderText.text(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initEditor() {
|
|
||||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
|
||||||
|
|
||||||
this.$widget.show();
|
|
||||||
|
|
||||||
this.$editor.on("click", e => this.handleEditorClick(e));
|
|
||||||
|
|
||||||
this.textEditor = await BalloonEditor.create(this.$editor[0], editorConfig);
|
|
||||||
this.textEditor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate());
|
|
||||||
|
|
||||||
// disable spellcheck for attribute editor
|
|
||||||
this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot()));
|
|
||||||
|
|
||||||
//await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector.js');
|
|
||||||
//CKEditorInspector.attach(this.textEditor);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleEditorClick(e) {
|
|
||||||
const pos = this.textEditor.model.document.selection.getFirstPosition();
|
|
||||||
|
|
||||||
if (pos && pos.textNode && pos.textNode.data) {
|
|
||||||
const clickIndex = this.getClickIndex(pos);
|
|
||||||
|
|
||||||
const parsedAttrs = attributesParser.lexAndParse(this.textEditor.getData(), true);
|
|
||||||
|
|
||||||
let matchedAttr = null;
|
|
||||||
|
|
||||||
for (const attr of parsedAttrs) {
|
|
||||||
if (clickIndex >= attr.startIndex && clickIndex <= attr.endIndex) {
|
|
||||||
matchedAttr = attr;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attributeDetailWidget.showAttributeDetail({
|
|
||||||
allAttributes: parsedAttrs,
|
|
||||||
attribute: matchedAttr,
|
|
||||||
isOwned: true,
|
|
||||||
x: e.pageX,
|
|
||||||
y: e.pageY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadReferenceLinkTitle(noteId, $el) {
|
|
||||||
const note = await treeCache.getNote(noteId, true);
|
|
||||||
|
|
||||||
let title;
|
|
||||||
|
|
||||||
if (!note) {
|
|
||||||
title = '[missing]';
|
|
||||||
}
|
|
||||||
else if (!note.isDeleted) {
|
|
||||||
title = note.title;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
title = note.isErased ? '[erased]' : `${note.title} (deleted)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
$el.text(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note) {
|
||||||
await this.renderOwnedAttributes(note.getOwnedAttributes());
|
const ownedAttributes = note.getOwnedAttributes();
|
||||||
|
|
||||||
|
this.$ownedExpanderText.text(ownedAttributes.length + ' owned ' + this.attrPlural(ownedAttributes.length));
|
||||||
|
|
||||||
const inheritedAttributes = note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
const inheritedAttributes = note.getAttributes().filter(attr => attr.noteId !== this.noteId);
|
||||||
|
|
||||||
@ -521,34 +157,12 @@ export default class AttributeListWidget extends TabAwareWidget {
|
|||||||
this.$inheritedAttributes.empty();
|
this.$inheritedAttributes.empty();
|
||||||
|
|
||||||
await this.renderInheritedAttributes(inheritedAttributes, this.$inheritedAttributes);
|
await this.renderInheritedAttributes(inheritedAttributes, this.$inheritedAttributes);
|
||||||
|
|
||||||
this.parseAttributes();
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderOwnedAttributes(ownedAttributes) {
|
|
||||||
const $attributesContainer = $("<div>");
|
|
||||||
|
|
||||||
for (const attribute of ownedAttributes) {
|
|
||||||
this.renderAttribute(attribute, $attributesContainer, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.spacedUpdate.allowUpdateWithoutChange(() => {
|
|
||||||
this.textEditor.setData($attributesContainer.html());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attrPlural(number) {
|
attrPlural(number) {
|
||||||
return 'attribute' + (number === 1 ? '' : 's');
|
return 'attribute' + (number === 1 ? '' : 's');
|
||||||
}
|
}
|
||||||
|
|
||||||
createNoteLink(noteId) {
|
|
||||||
return $("<a>", {
|
|
||||||
href: '#' + noteId,
|
|
||||||
class: 'reference-link',
|
|
||||||
'data-note-path': noteId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderInheritedAttributes(attributes, $container) {
|
renderInheritedAttributes(attributes, $container) {
|
||||||
for (const attribute of attributes) {
|
for (const attribute of attributes) {
|
||||||
const $span = $("<span>")
|
const $span = $("<span>")
|
||||||
@ -566,64 +180,7 @@ export default class AttributeListWidget extends TabAwareWidget {
|
|||||||
|
|
||||||
$container.append($span);
|
$container.append($span);
|
||||||
|
|
||||||
this.renderAttribute(attribute, $span, false);
|
attributeRenderer.renderAttribute(attribute, $span, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAttribute(attribute, $container, renderIsInheritable) {
|
|
||||||
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : '';
|
|
||||||
|
|
||||||
if (attribute.type === 'label') {
|
|
||||||
$container.append(document.createTextNode('#' + attribute.name + isInheritable));
|
|
||||||
|
|
||||||
if (attribute.value) {
|
|
||||||
$container.append('=');
|
|
||||||
$container.append(document.createTextNode(this.formatValue(attribute.value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
$container.append(' ');
|
|
||||||
} else if (attribute.type === 'relation') {
|
|
||||||
if (attribute.isAutoLink) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.value) {
|
|
||||||
$container.append(document.createTextNode('~' + attribute.name + isInheritable + "="));
|
|
||||||
$container.append(this.createNoteLink(attribute.value));
|
|
||||||
$container.append(" ");
|
|
||||||
} else {
|
|
||||||
ws.logError(`Relation ${attribute.attributeId} has empty target`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ws.logError("Unknown attr type: " + attribute.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formatValue(val) {
|
|
||||||
if (/^[\p{L}\p{N}\-_,.]+$/u.test(val)) {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
else if (!val.includes('"')) {
|
|
||||||
return '"' + val + '"';
|
|
||||||
}
|
|
||||||
else if (!val.includes("'")) {
|
|
||||||
return "'" + val + "'";
|
|
||||||
}
|
|
||||||
else if (!val.includes("`")) {
|
|
||||||
return "`" + val + "`";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return '"' + val.replace(/"/g, '\\"') + '"';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async focusOnAttributesEvent({tabId}) {
|
|
||||||
if (this.tabContext.tabId === tabId) {
|
|
||||||
this.$editor.trigger('focus');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAttributeListCommand({attributes}) {
|
|
||||||
this.renderOwnedAttributes(attributes);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user