mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	added in-editor help for editing attributes
This commit is contained in:
		| @@ -1,51 +1,56 @@ | |||||||
| import attributeParser from '../src/public/app/services/attribute_parser.js'; | import attributeParser from '../src/public/app/services/attribute_parser.js'; | ||||||
| import {describe, it, expect, execute} from './mini_test.js'; | import {describe, it, expect, execute} from './mini_test.js'; | ||||||
|  |  | ||||||
| describe("Lexer", () => { | describe("Lexing", () => { | ||||||
|     it("simple label", () => { |     it("simple label", () => { | ||||||
|         expect(attributeParser.lexer("#label").map(t => t.text)) |         expect(attributeParser.lex("#label").map(t => t.text)) | ||||||
|  |             .toEqual(["#label"]); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it("simple label with trailing spaces", () => { | ||||||
|  |         expect(attributeParser.lex("   #label  ").map(t => t.text)) | ||||||
|             .toEqual(["#label"]); |             .toEqual(["#label"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("inherited label", () => { |     it("inherited label", () => { | ||||||
|         expect(attributeParser.lexer("#label(inheritable)").map(t => t.text)) |         expect(attributeParser.lex("#label(inheritable)").map(t => t.text)) | ||||||
|             .toEqual(["#label", "(", "inheritable", ")"]); |             .toEqual(["#label", "(", "inheritable", ")"]); | ||||||
|  |  | ||||||
|         expect(attributeParser.lexer("#label ( inheritable ) ").map(t => t.text)) |         expect(attributeParser.lex("#label ( inheritable ) ").map(t => t.text)) | ||||||
|             .toEqual(["#label", "(", "inheritable", ")"]); |             .toEqual(["#label", "(", "inheritable", ")"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("label with value", () => { |     it("label with value", () => { | ||||||
|         expect(attributeParser.lexer("#label=Hallo").map(t => t.text)) |         expect(attributeParser.lex("#label=Hallo").map(t => t.text)) | ||||||
|             .toEqual(["#label", "=", "Hallo"]); |             .toEqual(["#label", "=", "Hallo"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("label with value", () => { |     it("label with value", () => { | ||||||
|         const tokens = attributeParser.lexer("#label=Hallo"); |         const tokens = attributeParser.lex("#label=Hallo"); | ||||||
|         expect(tokens[0].startIndex).toEqual(0); |         expect(tokens[0].startIndex).toEqual(0); | ||||||
|         expect(tokens[0].endIndex).toEqual(5); |         expect(tokens[0].endIndex).toEqual(5); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("relation with value", () => { |     it("relation with value", () => { | ||||||
|         expect(attributeParser.lexer('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map(t => t.text)) |         expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map(t => t.text)) | ||||||
|             .toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]); |             .toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("use quotes to define value", () => { |     it("use quotes to define value", () => { | ||||||
|         expect(attributeParser.lexer("#'label a'='hello\"` world'").map(t => t.text)) |         expect(attributeParser.lex("#'label a'='hello\"` world'").map(t => t.text)) | ||||||
|             .toEqual(["#label a", "=", 'hello"` world']); |             .toEqual(["#label a", "=", 'hello"` world']); | ||||||
|  |  | ||||||
|         expect(attributeParser.lexer('#"label a" = "hello\'` world"').map(t => t.text)) |         expect(attributeParser.lex('#"label a" = "hello\'` world"').map(t => t.text)) | ||||||
|             .toEqual(["#label a", "=", "hello'` world"]); |             .toEqual(["#label a", "=", "hello'` world"]); | ||||||
|  |  | ||||||
|         expect(attributeParser.lexer('#`label a` = `hello\'" world`').map(t => t.text)) |         expect(attributeParser.lex('#`label a` = `hello\'" world`').map(t => t.text)) | ||||||
|             .toEqual(["#label a", "=", "hello'\" world"]); |             .toEqual(["#label a", "=", "hello'\" world"]); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| describe("Parser", () => { | describe("Parser", () => { | ||||||
|     it("simple label", () => { |     it("simple label", () => { | ||||||
|         const attrs = attributeParser.parser(["#token"].map(t => ({text: t}))); |         const attrs = attributeParser.parse(["#token"].map(t => ({text: t}))); | ||||||
|  |  | ||||||
|         expect(attrs.length).toEqual(1); |         expect(attrs.length).toEqual(1); | ||||||
|         expect(attrs[0].type).toEqual('label'); |         expect(attrs[0].type).toEqual('label'); | ||||||
| @@ -55,7 +60,7 @@ describe("Parser", () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("inherited label", () => { |     it("inherited label", () => { | ||||||
|         const attrs = attributeParser.parser(["#token", "(", "inheritable", ")"].map(t => ({text: t}))); |         const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map(t => ({text: t}))); | ||||||
|  |  | ||||||
|         expect(attrs.length).toEqual(1); |         expect(attrs.length).toEqual(1); | ||||||
|         expect(attrs[0].type).toEqual('label'); |         expect(attrs[0].type).toEqual('label'); | ||||||
| @@ -65,7 +70,7 @@ describe("Parser", () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("label with value", () => { |     it("label with value", () => { | ||||||
|         const attrs = attributeParser.parser(["#token", "=", "val"].map(t => ({text: t}))); |         const attrs = attributeParser.parse(["#token", "=", "val"].map(t => ({text: t}))); | ||||||
|  |  | ||||||
|         expect(attrs.length).toEqual(1); |         expect(attrs.length).toEqual(1); | ||||||
|         expect(attrs[0].type).toEqual('label'); |         expect(attrs[0].type).toEqual('label'); | ||||||
| @@ -74,14 +79,14 @@ describe("Parser", () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it("relation", () => { |     it("relation", () => { | ||||||
|         let attrs = attributeParser.parser(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map(t => ({text: t}))); |         let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map(t => ({text: t}))); | ||||||
|  |  | ||||||
|         expect(attrs.length).toEqual(1); |         expect(attrs.length).toEqual(1); | ||||||
|         expect(attrs[0].type).toEqual('relation'); |         expect(attrs[0].type).toEqual('relation'); | ||||||
|         expect(attrs[0].name).toEqual("token"); |         expect(attrs[0].name).toEqual("token"); | ||||||
|         expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); |         expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); | ||||||
|  |  | ||||||
|         attrs = attributeParser.parser(["~token", "=", "#NFi2gL4xtPxM"].map(t => ({text: t}))); |         attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map(t => ({text: t}))); | ||||||
|  |  | ||||||
|         expect(attrs.length).toEqual(1); |         expect(attrs.length).toEqual(1); | ||||||
|         expect(attrs[0].type).toEqual('relation'); |         expect(attrs[0].type).toEqual('relation'); | ||||||
| @@ -97,6 +102,9 @@ describe("error cases", () => { | |||||||
|  |  | ||||||
|         expect(() => attributeParser.lexAndParse("#a&b/s")) |         expect(() => attributeParser.lexAndParse("#a&b/s")) | ||||||
|             .toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); |             .toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); | ||||||
|  |  | ||||||
|  |         expect(() => attributeParser.lexAndParse("#")) | ||||||
|  |             .toThrow(`Attribute name is empty, please fill the name.`); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| function lexer(str) { | function lex(str) { | ||||||
|  |     str = str.trim(); | ||||||
|  |  | ||||||
|     const tokens = []; |     const tokens = []; | ||||||
|  |  | ||||||
|     let quotes = false; |     let quotes = false; | ||||||
| @@ -106,12 +108,16 @@ function lexer(str) { | |||||||
| const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); | const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); | ||||||
|  |  | ||||||
| function checkAttributeName(attrName) { | function checkAttributeName(attrName) { | ||||||
|  |     if (attrName.length === 0) { | ||||||
|  |         throw new Error("Attribute name is empty, please fill the name."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (!attrNameMatcher.test(attrName)) { |     if (!attrNameMatcher.test(attrName)) { | ||||||
|         throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); |         throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| function parser(tokens, str, allowEmptyRelations = false) { | function parse(tokens, str, allowEmptyRelations = false) { | ||||||
|     const attrs = []; |     const attrs = []; | ||||||
|  |  | ||||||
|     function context(i) { |     function context(i) { | ||||||
| @@ -213,13 +219,13 @@ function parser(tokens, str, allowEmptyRelations = false) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function lexAndParse(str, allowEmptyRelations = false) { | function lexAndParse(str, allowEmptyRelations = false) { | ||||||
|     const tokens = lexer(str); |     const tokens = lex(str); | ||||||
|  |  | ||||||
|     return parser(tokens, str, allowEmptyRelations); |     return parse(tokens, str, allowEmptyRelations); | ||||||
| } | } | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|     lexer, |     lex, | ||||||
|     parser, |     parse, | ||||||
|     lexAndParse |     lexAndParse | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ function renderAttribute(attribute, $container, renderIsInheritable) { | |||||||
|             $container.append(document.createTextNode(formatValue(attribute.value))); |             $container.append(document.createTextNode(formatValue(attribute.value))); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         $container.append(' '); |         $container.append(" "); | ||||||
|     } else if (attribute.type === 'relation') { |     } else if (attribute.type === 'relation') { | ||||||
|         if (attribute.isAutoLink) { |         if (attribute.isAutoLink) { | ||||||
|             return; |             return; | ||||||
| @@ -20,7 +20,7 @@ function renderAttribute(attribute, $container, renderIsInheritable) { | |||||||
|         if (attribute.value) { |         if (attribute.value) { | ||||||
|             $container.append(document.createTextNode('~' + attribute.name + isInheritable + "=")); |             $container.append(document.createTextNode('~' + attribute.name + isInheritable + "=")); | ||||||
|             $container.append(createNoteLink(attribute.value)); |             $container.append(createNoteLink(attribute.value)); | ||||||
|             $container.append(" "); |             $container.append(" "); | ||||||
|         } else { |         } else { | ||||||
|             ws.logError(`Relation ${attribute.attributeId} has empty target`); |             ws.logError(`Relation ${attribute.attributeId} has empty target`); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -34,12 +34,12 @@ function setupGlobs() { | |||||||
|     <p> |     <p> | ||||||
|     <ul> |     <ul> | ||||||
|         <li>Just enter any text for full text search</li> |         <li>Just enter any text for full text search</li> | ||||||
|         <li><code>@abc</code> - returns notes with label abc</li> |         <li><code>#abc</code> - returns notes with label abc</li> | ||||||
|         <li><code>@year=2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li> |         <li><code>#year = 2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li> | ||||||
|         <li><code>@rock @pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li> |         <li><code>#rock #pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li> | ||||||
|         <li><code>@rock or @pop</code> - only one of the labels must be present</li> |         <li><code>#rock or #pop</code> - only one of the labels must be present</li> | ||||||
|         <li><code>@year<=2000</code> - numerical comparison (also >, >=, <).</li> |         <li><code>#year <= 2000</code> - numerical comparison (also >, >=, <).</li> | ||||||
|         <li><code>@dateCreated>=MONTH-1</code> - notes created in the last month</li> |         <li><code>note.dateCreated >= MONTH-1</code> - notes created in the last month</li> | ||||||
|         <li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li> |         <li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li> | ||||||
|     </ul> |     </ul> | ||||||
|     </p>`; |     </p>`; | ||||||
|   | |||||||
| @@ -7,6 +7,13 @@ import libraryLoader from "../services/library_loader.js"; | |||||||
| import treeCache from "../services/tree_cache.js"; | import treeCache from "../services/tree_cache.js"; | ||||||
| import attributeRenderer from "../services/attribute_renderer.js"; | import attributeRenderer from "../services/attribute_renderer.js"; | ||||||
|  |  | ||||||
|  | const HELP_TEXT = ` | ||||||
|  | <p>To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code></p>  | ||||||
|  |  | ||||||
|  | <p>For relation, type <code>~author = @</code> which should bring up an autocomplete where you can look up the desired note.</p> | ||||||
|  |  | ||||||
|  | <p>Alternatively you can add label and relation using the <code>+</code> button on the right side.</p>`; | ||||||
|  |  | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div style="position: relative"> | <div style="position: relative"> | ||||||
|     <style> |     <style> | ||||||
| @@ -170,7 +177,7 @@ const editorConfig = { | |||||||
|     toolbar: { |     toolbar: { | ||||||
|         items: [] |         items: [] | ||||||
|     }, |     }, | ||||||
|     placeholder: "Type the labels and relations here, e.g. #year=2020", |     placeholder: "Type the labels and relations here", | ||||||
|     mention: mentionSetup |     mention: mentionSetup | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -339,10 +346,10 @@ export default class AttributeEditorWidget extends TabAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async handleEditorClick(e) {console.log("click") |     async handleEditorClick(e) { | ||||||
|         const pos = this.textEditor.model.document.selection.getFirstPosition(); |         const pos = this.textEditor.model.document.selection.getFirstPosition(); | ||||||
|  |  | ||||||
|         if (pos && pos.textNode && pos.textNode.data) {console.log(pos); |         if (pos && pos.textNode && pos.textNode.data) { | ||||||
|             const clickIndex = this.getClickIndex(pos); |             const clickIndex = this.getClickIndex(pos); | ||||||
|  |  | ||||||
|             let parsedAttrs; |             let parsedAttrs; | ||||||
| @@ -350,7 +357,7 @@ export default class AttributeEditorWidget extends TabAwareWidget { | |||||||
|             try { |             try { | ||||||
|                 parsedAttrs = attributesParser.lexAndParse(this.getPreprocessedData(), true); |                 parsedAttrs = attributesParser.lexAndParse(this.getPreprocessedData(), true); | ||||||
|             } |             } | ||||||
|             catch (e) {console.log(e); |             catch (e) { | ||||||
|                 // the input is incorrect because user messed up with it and now needs to fix it manually |                 // the input is incorrect because user messed up with it and now needs to fix it manually | ||||||
|                 return null; |                 return null; | ||||||
|             } |             } | ||||||
| @@ -365,15 +372,37 @@ export default class AttributeEditorWidget extends TabAwareWidget { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             setTimeout(() => { |             setTimeout(() => { | ||||||
|                 this.attributeDetailWidget.showAttributeDetail({ |                 if (matchedAttr) { | ||||||
|                     allAttributes: parsedAttrs, |                     this.$editor.tooltip('hide'); | ||||||
|                     attribute: matchedAttr, |  | ||||||
|                     isOwned: true, |                     this.attributeDetailWidget.showAttributeDetail({ | ||||||
|                     x: e.pageX, |                         allAttributes: parsedAttrs, | ||||||
|                     y: e.pageY |                         attribute: matchedAttr, | ||||||
|                 }); |                         isOwned: true, | ||||||
|  |                         x: e.pageX, | ||||||
|  |                         y: e.pageY | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     this.showHelpTooltip(); | ||||||
|  |                 } | ||||||
|             }, 100); |             }, 100); | ||||||
|         } |         } | ||||||
|  |         else { | ||||||
|  |             this.showHelpTooltip(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     showHelpTooltip() {console.log("showHelpTooltip"); | ||||||
|  |         this.attributeDetailWidget.hide(); | ||||||
|  |  | ||||||
|  |         this.$editor.tooltip({ | ||||||
|  |             trigger: 'focus', | ||||||
|  |             html: true, | ||||||
|  |             title: HELP_TEXT, | ||||||
|  |             placement: 'bottom', | ||||||
|  |             offset: "0,20" | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getClickIndex(pos) { |     getClickIndex(pos) { | ||||||
| @@ -424,7 +453,7 @@ export default class AttributeEditorWidget extends TabAwareWidget { | |||||||
|                 attributeRenderer.renderAttribute(attribute, $attributesContainer, true); |                 attributeRenderer.renderAttribute(attribute, $attributesContainer, true); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |          | ||||||
|         this.textEditor.setData($attributesContainer.html()); |         this.textEditor.setData($attributesContainer.html()); | ||||||
|  |  | ||||||
|         if (saved) { |         if (saved) { | ||||||
| @@ -436,7 +465,12 @@ export default class AttributeEditorWidget extends TabAwareWidget { | |||||||
|  |  | ||||||
|     async focusOnAttributesEvent({tabId}) { |     async focusOnAttributesEvent({tabId}) { | ||||||
|         if (this.tabContext.tabId === tabId) { |         if (this.tabContext.tabId === tabId) { | ||||||
|             this.$editor.trigger('focus'); |             if (this.$editor.is(":visible")) { | ||||||
|  |                 this.$editor.trigger('focus'); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 this.triggerCommand('focusOnDetail', {tabId: this.tabContext.tabId}); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user