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 contextMenuService from "../services/context_menu.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 = `




5 inherited attributes

`; export default class NoteAttributesWidget extends TabAwareWidget { constructor() { super(); this.attributeDetailWidget = new AttributeDetailWidget().setParent(this); this.spacedUpdate = new SpacedUpdate(() => { this.parseAttributes(); this.attributeDetailWidget.hide(); }); } doRender() { this.$widget = $(TPL); this.$editor = this.$widget.find('.note-attributes-editor'); this.initialized = this.initEditor(); this.$attrDisplay = this.$widget.find('.attr-display'); this.$ownedExpander = this.$widget.find('.attr-owned-expander'); this.$ownedExpander.on('click', () => { if (this.$attrDisplay.is(":visible")) { this.$attrDisplay.slideUp(200); } else { this.$attrDisplay.slideDown(200); } }); this.$ownedExpanderText = this.$ownedExpander.find('.attr-expander-text'); this.$inheritedAttributes = this.$widget.find('.inherited-attributes'); this.$inheritedExpander = this.$widget.find('.attr-inherited-expander'); this.$inheritedExpander.on('click', () => { if (this.$inheritedAttributes.is(":visible")) { this.$inheritedAttributes.slideUp(200); } else { this.$inheritedAttributes.slideDown(200); } }); this.$inheritedExpanderText = this.$inheritedExpander.find('.attr-expander-text'); this.$inheritedEmptyExpander = this.$widget.find('.attr-inherited-empty-expander'); 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, items: [ {title: "Add new label", command: "addNewLabel", uiIcon: "hash"}, {title: "Add new relation", command: "addNewRelation", uiIcon: "transfer"}, {title: "----"}, {title: "Add new label definition", command: "addNewRelation", uiIcon: "empty"}, {title: "Add new relation definition", command: "addNewRelation", uiIcon: "empty"}, ], selectMenuItemHandler: ({command}) => { console.log(command); } }); }); 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) { await this.renderOwnedAttributes(note.getOwnedAttributes()); const inheritedAttributes = note.getAttributes().filter(attr => attr.noteId !== this.noteId); if (inheritedAttributes.length === 0) { this.$inheritedExpander.hide(); this.$inheritedEmptyExpander.show(); } else { this.$inheritedExpander.show(); this.$inheritedEmptyExpander.hide(); } this.$inheritedExpanderText.text(inheritedAttributes.length + ' inherited ' + this.attrPlural(inheritedAttributes.length)); this.$inheritedAttributes.empty(); await this.renderAttributesIntoDiv(inheritedAttributes, this.$inheritedAttributes); this.parseAttributes(); } async renderOwnedAttributes(ownedAttributes) { const $attributesContainer = $("
"); await this.renderAttributesIntoCKEditor(ownedAttributes, $attributesContainer); await this.spacedUpdate.allowUpdateWithoutChange(() => { this.textEditor.setData($attributesContainer.html()); }); } attrPlural(number) { return 'attribute' + (number === 1 ? '' : 's'); } createNoteLink(noteId) { return $("", { href: '#' + noteId, class: 'reference-link', 'data-note-path': noteId }); } async renderAttributesIntoCKEditor(attributes, $container) { for (const attribute of attributes) { this.renderAttribute(attribute, $container); } } renderAttributesIntoDiv(attributes, $container) { for (const attribute of attributes) { const $span = $("") .on('click', e => this.attributeDetailWidget.showAttributeDetail({ attribute: { noteId: attribute.noteId, type: attribute.type, name: attribute.name, value: attribute.value }, isOwned: false, x: e.pageX, y: e.pageY })); $container.append($span); this.renderAttribute(attribute, $span); } } renderAttribute(attribute, $container) { if (attribute.type === 'label') { $container.append(document.createTextNode('#' + attribute.name)); 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 + "=")); $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); } }