added in-editor help for editing attributes

This commit is contained in:
zadam 2020-08-21 23:08:53 +02:00
parent 0533b95562
commit ed6181a85e
5 changed files with 90 additions and 42 deletions

View File

@ -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.`);
}); });
}); });

View File

@ -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
} }

View File

@ -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`);
} }

View File

@ -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&lt;=2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li> <li><code>#year &lt;= 2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</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>`;

View File

@ -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});
}
} }
} }