Skip to content

Commit ed6181a

Browse files
committed
added in-editor help for editing attributes
1 parent 0533b95 commit ed6181a

File tree

5 files changed

+90
-42
lines changed

5 files changed

+90
-42
lines changed

spec-es6/attribute_parser.spec.js

+23-15
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,56 @@
11
import attributeParser from '../src/public/app/services/attribute_parser.js';
22
import {describe, it, expect, execute} from './mini_test.js';
33

4-
describe("Lexer", () => {
4+
describe("Lexing", () => {
55
it("simple label", () => {
6-
expect(attributeParser.lexer("#label").map(t => t.text))
6+
expect(attributeParser.lex("#label").map(t => t.text))
7+
.toEqual(["#label"]);
8+
});
9+
10+
it("simple label with trailing spaces", () => {
11+
expect(attributeParser.lex(" #label ").map(t => t.text))
712
.toEqual(["#label"]);
813
});
914

1015
it("inherited label", () => {
11-
expect(attributeParser.lexer("#label(inheritable)").map(t => t.text))
16+
expect(attributeParser.lex("#label(inheritable)").map(t => t.text))
1217
.toEqual(["#label", "(", "inheritable", ")"]);
1318

14-
expect(attributeParser.lexer("#label ( inheritable ) ").map(t => t.text))
19+
expect(attributeParser.lex("#label ( inheritable ) ").map(t => t.text))
1520
.toEqual(["#label", "(", "inheritable", ")"]);
1621
});
1722

1823
it("label with value", () => {
19-
expect(attributeParser.lexer("#label=Hallo").map(t => t.text))
24+
expect(attributeParser.lex("#label=Hallo").map(t => t.text))
2025
.toEqual(["#label", "=", "Hallo"]);
2126
});
2227

2328
it("label with value", () => {
24-
const tokens = attributeParser.lexer("#label=Hallo");
29+
const tokens = attributeParser.lex("#label=Hallo");
2530
expect(tokens[0].startIndex).toEqual(0);
2631
expect(tokens[0].endIndex).toEqual(5);
2732
});
2833

2934
it("relation with value", () => {
30-
expect(attributeParser.lexer('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map(t => t.text))
35+
expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map(t => t.text))
3136
.toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]);
3237
});
3338

3439
it("use quotes to define value", () => {
35-
expect(attributeParser.lexer("#'label a'='hello\"` world'").map(t => t.text))
40+
expect(attributeParser.lex("#'label a'='hello\"` world'").map(t => t.text))
3641
.toEqual(["#label a", "=", 'hello"` world']);
3742

38-
expect(attributeParser.lexer('#"label a" = "hello\'` world"').map(t => t.text))
43+
expect(attributeParser.lex('#"label a" = "hello\'` world"').map(t => t.text))
3944
.toEqual(["#label a", "=", "hello'` world"]);
4045

41-
expect(attributeParser.lexer('#`label a` = `hello\'" world`').map(t => t.text))
46+
expect(attributeParser.lex('#`label a` = `hello\'" world`').map(t => t.text))
4247
.toEqual(["#label a", "=", "hello'\" world"]);
4348
});
4449
});
4550

4651
describe("Parser", () => {
4752
it("simple label", () => {
48-
const attrs = attributeParser.parser(["#token"].map(t => ({text: t})));
53+
const attrs = attributeParser.parse(["#token"].map(t => ({text: t})));
4954

5055
expect(attrs.length).toEqual(1);
5156
expect(attrs[0].type).toEqual('label');
@@ -55,7 +60,7 @@ describe("Parser", () => {
5560
});
5661

5762
it("inherited label", () => {
58-
const attrs = attributeParser.parser(["#token", "(", "inheritable", ")"].map(t => ({text: t})));
63+
const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map(t => ({text: t})));
5964

6065
expect(attrs.length).toEqual(1);
6166
expect(attrs[0].type).toEqual('label');
@@ -65,7 +70,7 @@ describe("Parser", () => {
6570
});
6671

6772
it("label with value", () => {
68-
const attrs = attributeParser.parser(["#token", "=", "val"].map(t => ({text: t})));
73+
const attrs = attributeParser.parse(["#token", "=", "val"].map(t => ({text: t})));
6974

7075
expect(attrs.length).toEqual(1);
7176
expect(attrs[0].type).toEqual('label');
@@ -74,14 +79,14 @@ describe("Parser", () => {
7479
});
7580

7681
it("relation", () => {
77-
let attrs = attributeParser.parser(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map(t => ({text: t})));
82+
let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map(t => ({text: t})));
7883

7984
expect(attrs.length).toEqual(1);
8085
expect(attrs[0].type).toEqual('relation');
8186
expect(attrs[0].name).toEqual("token");
8287
expect(attrs[0].value).toEqual('NFi2gL4xtPxM');
8388

84-
attrs = attributeParser.parser(["~token", "=", "#NFi2gL4xtPxM"].map(t => ({text: t})));
89+
attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map(t => ({text: t})));
8590

8691
expect(attrs.length).toEqual(1);
8792
expect(attrs[0].type).toEqual('relation');
@@ -97,6 +102,9 @@ describe("error cases", () => {
97102

98103
expect(() => attributeParser.lexAndParse("#a&b/s"))
99104
.toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
105+
106+
expect(() => attributeParser.lexAndParse("#"))
107+
.toThrow(`Attribute name is empty, please fill the name.`);
100108
});
101109
});
102110

src/public/app/services/attribute_parser.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
function lexer(str) {
1+
function lex(str) {
2+
str = str.trim();
3+
24
const tokens = [];
35

46
let quotes = false;
@@ -106,12 +108,16 @@ function lexer(str) {
106108
const attrNameMatcher = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
107109

108110
function checkAttributeName(attrName) {
111+
if (attrName.length === 0) {
112+
throw new Error("Attribute name is empty, please fill the name.");
113+
}
114+
109115
if (!attrNameMatcher.test(attrName)) {
110116
throw new Error(`Attribute name "${attrName}" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
111117
}
112118
}
113119

114-
function parser(tokens, str, allowEmptyRelations = false) {
120+
function parse(tokens, str, allowEmptyRelations = false) {
115121
const attrs = [];
116122

117123
function context(i) {
@@ -213,13 +219,13 @@ function parser(tokens, str, allowEmptyRelations = false) {
213219
}
214220

215221
function lexAndParse(str, allowEmptyRelations = false) {
216-
const tokens = lexer(str);
222+
const tokens = lex(str);
217223

218-
return parser(tokens, str, allowEmptyRelations);
224+
return parse(tokens, str, allowEmptyRelations);
219225
}
220226

221227
export default {
222-
lexer,
223-
parser,
228+
lex,
229+
parse,
224230
lexAndParse
225231
}

src/public/app/services/attribute_renderer.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function renderAttribute(attribute, $container, renderIsInheritable) {
1111
$container.append(document.createTextNode(formatValue(attribute.value)));
1212
}
1313

14-
$container.append(' ');
14+
$container.append(" ");
1515
} else if (attribute.type === 'relation') {
1616
if (attribute.isAutoLink) {
1717
return;
@@ -20,7 +20,7 @@ function renderAttribute(attribute, $container, renderIsInheritable) {
2020
if (attribute.value) {
2121
$container.append(document.createTextNode('~' + attribute.name + isInheritable + "="));
2222
$container.append(createNoteLink(attribute.value));
23-
$container.append(" ");
23+
$container.append(" ");
2424
} else {
2525
ws.logError(`Relation ${attribute.attributeId} has empty target`);
2626
}

src/public/app/services/glob.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ function setupGlobs() {
3434
<p>
3535
<ul>
3636
<li>Just enter any text for full text search</li>
37-
<li><code>@abc</code> - returns notes with label abc</li>
38-
<li><code>@year=2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li>
39-
<li><code>@rock @pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li>
40-
<li><code>@rock or @pop</code> - only one of the labels must be present</li>
41-
<li><code>@year&lt;=2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
42-
<li><code>@dateCreated>=MONTH-1</code> - notes created in the last month</li>
37+
<li><code>#abc</code> - returns notes with label abc</li>
38+
<li><code>#year = 2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li>
39+
<li><code>#rock #pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li>
40+
<li><code>#rock or #pop</code> - only one of the labels must be present</li>
41+
<li><code>#year &lt;= 2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
42+
<li><code>note.dateCreated >= MONTH-1</code> - notes created in the last month</li>
4343
<li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li>
4444
</ul>
4545
</p>`;

src/public/app/widgets/attribute_editor.js

+47-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import libraryLoader from "../services/library_loader.js";
77
import treeCache from "../services/tree_cache.js";
88
import attributeRenderer from "../services/attribute_renderer.js";
99

10+
const HELP_TEXT = `
11+
<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>
12+
13+
<p>For relation, type <code>~author = @</code> which should bring up an autocomplete where you can look up the desired note.</p>
14+
15+
<p>Alternatively you can add label and relation using the <code>+</code> button on the right side.</p>`;
16+
1017
const TPL = `
1118
<div style="position: relative">
1219
<style>
@@ -170,7 +177,7 @@ const editorConfig = {
170177
toolbar: {
171178
items: []
172179
},
173-
placeholder: "Type the labels and relations here, e.g. #year=2020",
180+
placeholder: "Type the labels and relations here",
174181
mention: mentionSetup
175182
};
176183

@@ -339,18 +346,18 @@ export default class AttributeEditorWidget extends TabAwareWidget {
339346
}
340347
}
341348

342-
async handleEditorClick(e) {console.log("click")
349+
async handleEditorClick(e) {
343350
const pos = this.textEditor.model.document.selection.getFirstPosition();
344351

345-
if (pos && pos.textNode && pos.textNode.data) {console.log(pos);
352+
if (pos && pos.textNode && pos.textNode.data) {
346353
const clickIndex = this.getClickIndex(pos);
347354

348355
let parsedAttrs;
349356

350357
try {
351358
parsedAttrs = attributesParser.lexAndParse(this.getPreprocessedData(), true);
352359
}
353-
catch (e) {console.log(e);
360+
catch (e) {
354361
// the input is incorrect because user messed up with it and now needs to fix it manually
355362
return null;
356363
}
@@ -365,15 +372,37 @@ export default class AttributeEditorWidget extends TabAwareWidget {
365372
}
366373

367374
setTimeout(() => {
368-
this.attributeDetailWidget.showAttributeDetail({
369-
allAttributes: parsedAttrs,
370-
attribute: matchedAttr,
371-
isOwned: true,
372-
x: e.pageX,
373-
y: e.pageY
374-
});
375+
if (matchedAttr) {
376+
this.$editor.tooltip('hide');
377+
378+
this.attributeDetailWidget.showAttributeDetail({
379+
allAttributes: parsedAttrs,
380+
attribute: matchedAttr,
381+
isOwned: true,
382+
x: e.pageX,
383+
y: e.pageY
384+
});
385+
}
386+
else {
387+
this.showHelpTooltip();
388+
}
375389
}, 100);
376390
}
391+
else {
392+
this.showHelpTooltip();
393+
}
394+
}
395+
396+
showHelpTooltip() {console.log("showHelpTooltip");
397+
this.attributeDetailWidget.hide();
398+
399+
this.$editor.tooltip({
400+
trigger: 'focus',
401+
html: true,
402+
title: HELP_TEXT,
403+
placement: 'bottom',
404+
offset: "0,20"
405+
});
377406
}
378407

379408
getClickIndex(pos) {
@@ -424,7 +453,7 @@ export default class AttributeEditorWidget extends TabAwareWidget {
424453
attributeRenderer.renderAttribute(attribute, $attributesContainer, true);
425454
}
426455
}
427-
456+
428457
this.textEditor.setData($attributesContainer.html());
429458

430459
if (saved) {
@@ -436,7 +465,12 @@ export default class AttributeEditorWidget extends TabAwareWidget {
436465

437466
async focusOnAttributesEvent({tabId}) {
438467
if (this.tabContext.tabId === tabId) {
439-
this.$editor.trigger('focus');
468+
if (this.$editor.is(":visible")) {
469+
this.$editor.trigger('focus');
470+
}
471+
else {
472+
this.triggerCommand('focusOnDetail', {tabId: this.tabContext.tabId});
473+
}
440474
}
441475
}
442476

0 commit comments

Comments
 (0)