mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
added in-editor help for editing attributes
This commit is contained in:
parent
0533b95562
commit
ed6181a85e
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user