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 {describe, it, expect, execute} from './mini_test.js';
describe("Lexer", () => {
describe("Lexing", () => {
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"]);
});
it("inherited label", () => {
expect(attributeParser.lexer("#label(inheritable)").map(t => t.text))
expect(attributeParser.lex("#label(inheritable)").map(t => t.text))
.toEqual(["#label", "(", "inheritable", ")"]);
expect(attributeParser.lexer("#label ( inheritable ) ").map(t => t.text))
expect(attributeParser.lex("#label ( inheritable ) ").map(t => t.text))
.toEqual(["#label", "(", "inheritable", ")"]);
});
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"]);
});
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].endIndex).toEqual(5);
});
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"]);
});
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']);
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"]);
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"]);
});
});
describe("Parser", () => {
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[0].type).toEqual('label');
@ -55,7 +60,7 @@ describe("Parser", () => {
});
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[0].type).toEqual('label');
@ -65,7 +70,7 @@ describe("Parser", () => {
});
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[0].type).toEqual('label');
@ -74,14 +79,14 @@ describe("Parser", () => {
});
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[0].type).toEqual('relation');
expect(attrs[0].name).toEqual("token");
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[0].type).toEqual('relation');
@ -97,6 +102,9 @@ describe("error cases", () => {
expect(() => attributeParser.lexAndParse("#a&b/s"))
.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 = [];
let quotes = false;
@ -106,12 +108,16 @@ function lexer(str) {
const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
function checkAttributeName(attrName) {
if (attrName.length === 0) {
throw new Error("Attribute name is empty, please fill the name.");
}
if (!attrNameMatcher.test(attrName)) {
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 = [];
function context(i) {
@ -213,13 +219,13 @@ function parser(tokens, 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 {
lexer,
parser,
lex,
parse,
lexAndParse
}

View file

@ -11,7 +11,7 @@ function renderAttribute(attribute, $container, renderIsInheritable) {
$container.append(document.createTextNode(formatValue(attribute.value)));
}
$container.append(' ');
$container.append(" ");
} else if (attribute.type === 'relation') {
if (attribute.isAutoLink) {
return;
@ -20,7 +20,7 @@ function renderAttribute(attribute, $container, renderIsInheritable) {
if (attribute.value) {
$container.append(document.createTextNode('~' + attribute.name + isInheritable + "="));
$container.append(createNoteLink(attribute.value));
$container.append(" ");
$container.append(" ");
} else {
ws.logError(`Relation ${attribute.attributeId} has empty target`);
}

View file

@ -34,12 +34,12 @@ function setupGlobs() {
<p>
<ul>
<li>Just enter any text for full text search</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>@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>@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>#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>#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>#year &lt;= 2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</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>
</ul>
</p>`;

View file

@ -7,6 +7,13 @@ import libraryLoader from "../services/library_loader.js";
import treeCache from "../services/tree_cache.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 = `
<div style="position: relative">
<style>
@ -170,7 +177,7 @@ const editorConfig = {
toolbar: {
items: []
},
placeholder: "Type the labels and relations here, e.g. #year=2020",
placeholder: "Type the labels and relations here",
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();
if (pos && pos.textNode && pos.textNode.data) {console.log(pos);
if (pos && pos.textNode && pos.textNode.data) {
const clickIndex = this.getClickIndex(pos);
let parsedAttrs;
@ -350,7 +357,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
try {
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
return null;
}
@ -365,15 +372,37 @@ export default class AttributeEditorWidget extends TabAwareWidget {
}
setTimeout(() => {
this.attributeDetailWidget.showAttributeDetail({
allAttributes: parsedAttrs,
attribute: matchedAttr,
isOwned: true,
x: e.pageX,
y: e.pageY
});
if (matchedAttr) {
this.$editor.tooltip('hide');
this.attributeDetailWidget.showAttributeDetail({
allAttributes: parsedAttrs,
attribute: matchedAttr,
isOwned: true,
x: e.pageX,
y: e.pageY
});
}
else {
this.showHelpTooltip();
}
}, 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) {
@ -424,7 +453,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
attributeRenderer.renderAttribute(attribute, $attributesContainer, true);
}
}
this.textEditor.setData($attributesContainer.html());
if (saved) {
@ -436,7 +465,12 @@ export default class AttributeEditorWidget extends TabAwareWidget {
async focusOnAttributesEvent({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});
}
}
}