Commit d2c07460 authored by ransome1's avatar ransome1
Browse files

GUI refresh

parent fe080b66
......@@ -92,13 +92,13 @@
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"scripts": {
"pack": "yarn build:css && electron-builder --dir",
"build:windows": "yarn build:css && electron-builder -w --publish never",
"build:macos": "yarn build:css && electron-builder -m --publish never",
"build:linux": "yarn build:css && electron-builder -l --publish never",
"build:appx": "yarn build:css && electron-builder -w appx --publish never",
"build:windows": "yarn build:css && yarn build:pegjs && electron-builder -w --publish never",
"build:macos": "yarn build:css && yarn build:pegjs && electron-builder -m --publish never",
"build:linux": "yarn build:css && yarn build:pegjs && electron-builder -l --publish never",
"build:appx": "yarn build:css && yarn build:pegjs && electron-builder -w appx --publish never",
"build:appimage": "yarn build:css && yarn build:pegjs && electron-builder -l AppImage --publish never",
"build:css": "sass src/scss/style.scss:src/css/style.css",
"build:pegjs": "peggy --format es --output src/js/filterlang.mjs src/js/filterlang.pegjs",
"pack": "yarn build:css && yarn build:pegjs && electron-builder --dir",
"lint": "eslint --ext .js, src --ext .mjs, src",
"test": "mocha --timeout 10000",
"test1": "mocha ./test/onboarding.js --timeout 10000",
......
This diff is collapsed.
This diff is collapsed.
......@@ -27,7 +27,7 @@
<li><a href="#" id="navBtnAddTodo" tabindex="0"><i class="fas fa-plus"></i></a></li>
<li><a href="#" id="navBtnFilter" class="drawerTrigger" data-drawer="filterDrawer" tabindex="0"><i class="fas fa-filter"></i></a></li>
<li><a href="#" id="navBtnView" class="drawerTrigger" data-drawer="viewDrawer"tabindex="0"><i class="fas fa-sliders-h"></i></a></li>
<li><a href="#" id="btnOpenTodoFile" tabindex="-1"><i class="fas fa-folder-open"></i></a></li>
<li><a href="#" id="btnOpenTodoFile" tabindex="0"><i class="fas fa-folder-open"></i></a></li>
<li><a href="#" id="btnTheme" tabindex="-1"><i class="fas fa-adjust"></i></a></li>
</ul>
<ul>
......@@ -38,7 +38,7 @@
<section id="filterDrawer" class="drawer dropdown" tabindex="0">
<div class="container">
<div id="todoFilters"></div>
<button id="btnFiltersResetFilters" class="button btnResetFilters" tabindex="0"><i class="fas fa-ban"></i>&nbsp;<span></span></button>
<button id="btnFiltersResetFilters" class="btnResetFilters" tabindex="0"><i class="fas fa-ban"></i>&nbsp;<span></span></button>
</div>
</section>
<section id="viewDrawer" class="drawer dropdown" tabindex="0">
......@@ -189,14 +189,14 @@
<section id="todoTableSearchContainer" class="inputWrapper">
<i class="fas fa-search"></i>
<label id="todoTableSearchLabel" for="todoTableSearch">Search by todo.txt syntax</label>
<label id="todoTableSearchLabel" for="todoTableSearch"></label>
<input id="todoTableSearch" class="input is-medium" type="search" tabindex="1" placeholder="(A) Todo text +project @context due:2020-12-12 rec:d">
<a href="#" id="todoTableSearchAddTodo" class="tag" tabindex="5"><i class="fas fa-plus"></i>&nbsp;Add as new todo</a>
<section id="resultStats">
<span class="tag"></span>
</section>
</section>
<section id="resultStats">
<span class="tag"></span>
</section>
<section id="todoTable" tabindex="-1"></section>
......@@ -219,7 +219,7 @@
<h1 id="addTodoContainerHeadline" class="title is-1"></h1>
<p id="addTodoContainerSubtitle" class="subtitle"></p>
<p class="file is-boxed is-centered">
<a href="#" id="btnAddTodoContainer" class="btnOnboarding">
<a href="#" id="btnAddTodoContainer" class="btnOnboarding" tabindex="0">
<i class="fas fa-plus"></i>
<span id="addTodoContainerButton" class="file-label"></span>
</a>
......@@ -229,25 +229,23 @@
<section id="noResultContainer" class="contentContainer">
<h1 id="noResultContainerHeadline" class="title is-1"></h1>
<p id="noResultContainerSubtitle" class="subtitle"></p>
<p><button id="btnNoResultContainerResetFilters" class="btnResetFilters button" tabindex="0"><i class="fas fa-ban"></i>&nbsp;<span></span></button></p>
<p><button id="btnNoResultContainerResetFilters" class="btnResetFilters" tabindex="0"><i class="fas fa-ban"></i>&nbsp;<span></span></button></p>
</section>
</div>
</div>
<form id="modalForm" class="modal">
<section id="modalForm" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header id="modalTitle" class="card-header-title"></header>
<div class="card-content">
<div class="inputWrapper">
<label id="modalFormInputLabel" for="modalFormInput">Search by todo.txt syntax</label>
<label id="modalFormInputLabel" for="modalFormInput"></label>
<input id="modalFormInput" class="input is-medium" type="text" tabindex="0" placeholder="(A) Todo text +project @context due:2020-12-12 rec:d">
<a href="#" id="modalFormInputResize" class="icon is-right" tabindex="-1" data-input-type="input"><i class="fas fa-expand-alt"></i></a>
</div>
<div id="autoCompleteContainer" class="card"></div>
<section id="autoCompleteContainer" class="card"></section>
<article class="message">
<div id="modalFormAlert" class="message-body"></div>
</article>
......@@ -342,13 +340,13 @@
</div>
<footer class="card-footer">
<button id="btnSave" type="submit" class="card-footer-item" tabindex="0"></button>
<a href="#" id="btnItemStatus" class="card-footer-item" tabindex="0"></a>
<a href="#" class="card-footer-item btnModalCancel" tabindex="0"></a>
<button id="btnSave" class="card-footer-item" tabindex="0"></button>
<button id="btnItemStatus" class="card-footer-item" tabindex="0"></button>
<button class="card-footer-item" role="cancel" tabindex="0"></button>
</footer>
</div>
</div>
</form>
</section>
<div id="modalSettings" class="modal content" tabindex="0">
<div class="modal-background"></div>
......@@ -662,11 +660,11 @@
<div id="filterContext">
<div class="card">
<div class="card-content">
<input id="filterContextInput" class="input" type="text">
<input id="filterContextInput" class="input" type="text" tabindex="0">
</div>
<footer class="card-footer">
<a href="#" id="filterContextSave" class="card-footer-item"></a>
<a href="#" id="filterContextDelete" class="card-footer-item"></a>
<button id="filterContextSave" class="card-footer-item" tabindex="0"></button>
<button id="filterContextDelete" class="card-footer-item" tabindex="0"></button
</footer>
</div>
</div>
......@@ -675,16 +673,13 @@
<div class="modal-background"></div>
<div class="modal-content">
<div class="card">
<header>
<p id="modalChangeFileTitle" class="card-header-title"></p>
</header>
<div class="card-content">
<table id="modalChangeFileTable"></table>
</div>
<footer class="card-footer">
<a href="#" id="btnFilesOpenTodoFile" class="card-footer-item" tabindex="0"><i class="fas fa-folder-open"></i>&nbsp;<span id="modalChangeFileOpen"></span></a>
<a href="#" id="btnFilesCreateTodoFile" class="card-footer-item" tabindex="0"><i class="fas fa-plus-circle"></i>&nbsp;<span id="modalChangeFileCreate"></span></a>
<a href="#" class="card-footer-item btnModalCancel" tabindex="0"></a>
<button id="btnFilesOpenTodoFile" class="card-footer-item" tabindex="0"><i class="fas fa-folder-open"></i>&nbsp;<span id="modalChangeFileOpen"></span></button>
<button id="btnFilesCreateTodoFile" class="card-footer-item" tabindex="0"><i class="fas fa-plus-circle"></i>&nbsp;<span id="modalChangeFileCreate"></span></button>
<button class="card-footer-item" role="cancel" tabindex="0"></button>
</footer>
</div>
</div>
......@@ -696,8 +691,8 @@
<div class="card">
<div id="modalPromptContent" class="card-content"></div>
<footer class="card-footer">
<a href="#" id="modalPromptConfirm" class="card-footer-item" tabindex="0">Confirm</a>
<a href="#" id="modalPromptCancel" class="card-footer-item" tabindex="0">Cancel</a>
<button id="modalPromptConfirm" class="card-footer-item" tabindex="0"></a>
<button id="modalPromptCancel" role="cancel" class="card-footer-item" tabindex="0"></a>
</footer>
</div>
</div>
......@@ -708,25 +703,25 @@
<article class="message fixed" data="logging">
<div class="message-header">
<p><span id="messageLoggingTitle"></span></p>
<button class="delete close" aria-label="delete" data-message="logging"></button>
<button class="delete close" aria-label="delete" data-message="logging" tabindex="-1"></button>
</div>
<div class="message-body">
<p><span id="messageLoggingBody"></span></p>
<p><button id="btnMessageLogging" class="button" tabindex="0"><i class="fas fa-cog"></i>&nbsp;<span id="messageLoggingButton"></span></button></p>
<p><button id="btnMessageLogging" tabindex="-1"><i class="fas fa-cog"></i>&nbsp;<span id="messageLoggingButton"></span></button></p>
</div>
</article>
<article class="message fixed" data="share">
<div class="message-header">
<p><span id="messageShareTitle"></span></p>
<button class="delete close" aria-label="delete" data-message="share"></button>
<button class="delete close" aria-label="delete" data-message="share" tabindex="-1"></button>
</div>
<div class="message-body">
<p><span id="messageShareBody"></span></p>
<p class="brands">
<a href="https://twitter.com/intent/tweet?text=Check%20out%20sleek,%20a%20todo%20app%20based%20on%20todo.txt,%20free%20and%20open-source.%20Available%20for%20Linux,%20Windows%20and%20MacOS.%20https://github.com/ransome1/sleek" target="_blank"><i class="fab fa-twitter"></i></a>
<a href="https://www.facebook.com/sharer/sharer.php?u=https://github.com/ransome1/sleek" target="_blank"><i class="fab fa-facebook-square"></i></a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=https://github.com/ransome1/sleek" target="_blank"><i class="fab fa-linkedin"></i></a>
<a tabindex="-1" href="https://twitter.com/intent/tweet?text=Check%20out%20sleek,%20a%20todo%20app%20based%20on%20todo.txt,%20free%20and%20open-source.%20Available%20for%20Linux,%20Windows%20and%20MacOS.%20https://github.com/ransome1/sleek" target="_blank"><i class="fab fa-twitter"></i></a>
<a tabindex="-1" href="https://www.facebook.com/sharer/sharer.php?u=https://github.com/ransome1/sleek" target="_blank"><i class="fab fa-facebook-square"></i></a>
<a tabindex="-1" href="https://www.linkedin.com/shareArticle?mini=true&url=https://github.com/ransome1/sleek" target="_blank"><i class="fab fa-linkedin"></i></a>
</p>
</div>
</article>
......
"use strict";
import { resetModal, handleError, userData, setUserData, translations } from "../render.js";
import { resetFilters, resetModal, handleError, userData, setUserData, translations } from "../render.js";
import { _paq } from "./matomo.mjs";
import { createModalJail } from "../configs/modal.config.mjs";
......@@ -22,11 +22,15 @@ function showFiles() {
let cell3 = row.insertCell(2);
row.setAttribute("data-path", files[file][1]);
if(files[file][0]===1) {
cell1.innerHTML = "<button class=\"button\" disabled>" + translations.selected + "</button>";
cell1.innerHTML = "<button disabled>" + translations.selected + "</button>";
} else {
cell1.innerHTML = "<button class=\"button is-link\" tabindex=\"0\">" + translations.select + "</button>";
cell1.innerHTML = "<button tabindex=\"0\">" + translations.select + "</button>";
cell1.onclick = function() {
setUserData("selectedFilters", []);
resetFilters().then(function(response) {
console.info(response);
}).catch(function(error) {
handleError(error);
});
resetModal().then(response => {
window.api.send("startFileWatcher", this.parentElement.getAttribute("data-path"));
console.info(response);
......
This diff is collapsed.
{{
import { addIntervalToDate } from "./recurrences.mjs";
}}
filterQuery
= _ left:orExpr _ { return left; }
/ _ { return []; }
orExpr
= left:andExpr _ OrOp _ right:orExpr { return left.concat(right, ["||"]); }
/ left:andExpr { return left; }
andExpr
= left:notExpr _ AndOp _ right:andExpr { return left.concat(right, ["&&"]); }
/ left:notExpr { return left; }
notExpr
= NotOp _ left:notExpr { return left.concat(["!!"]); }
/ left:boolExpr { return left; }
boolExpr
= left:project { return left; }
/ left:context { return left; }
/ "(" _ left:orExpr _ ")" { return left; }
/ left:comparison { return left; }
/ "complete" { return ["complete"]; }
/ left:StringLiteral { return ["string", left]; }
/ left:RegexLiteral { return ["regex", left]; }
project
= "+" left:name { return ["++", left]; }
/ "+" { return ["++", "*"]; }
context
= "@" left:name { return ["@@", left]; }
/ "@" { return ["@@", "*"]; }
OrOp
= "||"
/ "or"i
AndOp
= "&&"
/ "and"i
NotOp
= "!"
/ "not"i
comparison
= left:priorityComparison { return left; }
/ left:dueComparison { return left; }
priorityComparison
= priorityKeyword _ op:compareOp _ right:priorityLiteral { return ["priority", right, op]; }
/ priorityKeyword { return ["priority"]; }
priorityLiteral
= [A-Z] { return text(); }
priorityKeyword
= "pri" ("o" ("r" ("i" ("t" "y"?)?)?)?)?
dueComparison
= "due" _ op:compareOp _ right:dateExpr { return ["due"].concat(right, [op]); }
/ "due" { return ["due"]; }
dateExpr
= left:dateLiteral _ op:dateOp _ count:number unit:[dbwmy] {
if (op == "-") {
count = count * -1;
}
// we do our date math with the same code as we use for
// recurrence calculations. All dates are returned from
// the parser as millisec since epoch (getTime()) to
// simplify comparisons in the filter lang execution engine.
let d = addIntervalToDate(new Date(left), count, unit);
return d.getTime();
}
/ left:dateLiteral { return left; }
dateOp
= "+" { return text(); }
/ "-" { return text(); }
compareOp
= "==" { return text(); }
/ "!=" { return text(); }
/ ">=" { return text(); }
/ "<=" { return text(); }
/ ">" { return text(); }
/ "<" { return text(); }
dateLiteral
= year:number4 "-" month: number2 "-" day:number2 {
let d = new Date(year, month-1, day);
return d.getTime();
}
/ "today" {
let d = new Date(); // now, w current time of day
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return d.getTime();
}
/ "tomorrow" {
let d = new Date(); // now, w current time of day
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return d.getTime() + 24*60*60*1000;
}
/ "yesterday" {
let d = new Date(); // now, w current time of day
d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
return d.getTime() - 24*60*60*1000;
}
number4
= [0-9][0-9][0-9][0-9] { return text(); }
number2
= [0-9][0-9] { return text(); }
number
= [0-9]+ { return text(); }
StringLiteral "string"
= '"' chars:DoubleStringCharacter* '"'? {
return chars.join("");
}
/ "'" chars:SingleStringCharacter* "'"? {
return chars.join("");
}
DoubleStringCharacter
= '\\' '"' { return '"'; }
/ !'"' SourceCharacter { return text(); }
SingleStringCharacter
= '\\' "'" { return "'"; }
/ !"'" SourceCharacter { return text(); }
RegexLiteral "regex"
= "/" chars:RegexCharacter* "/" "i" {
return new RegExp(chars.join(""), "i");
}
/ "/" chars:RegexCharacter* "/"? {
return new RegExp(chars.join(""));
}
RegexCharacter
= "\\" "/" { return "/"; }
/ !"/" SourceCharacter { return text(); }
SourceCharacter
= .
name
= '"' nonblank+ '"' { return text(); }
/ nonblank+ '"' { return '"' + text(); }
/ nonblank+ { return text(); }
nonblank
= [^ \t\n\r"()]
_ "whitespace"
= [ \t\n\r]*
// This is a simple stack machine that executes a filter language query
// compiled by filterlang.pegjs (which generates filterlang.mjs).
// The compiled query consists of a list of postfix opcodes designed
// specifically for todo.txt searching and filtering.
function runQuery(item, compiledQuery) {
if (!compiledQuery) {
return true; // a null query matches everything
}
let stack = [];
let operand1 = false;
let operand2 = false;
let next = 0;
let q = compiledQuery.slice(); // shallow copy
while (q.length > 0) {
const opcode = q.shift();
switch(opcode) {
case "priority":
stack.push(item.priority);
break;
case "due":
let d = item.due;
if (d) {
// normalize date to have time of midnight in local zone
// we represent dates as millisec from epoch to simplify comparison
d = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
}
stack.push(d);
break;
case "complete":
stack.push(item.complete);
break;
case "string":
next = q.shift(); // the string value to match
stack.push(item.toString().toLowerCase().indexOf(next.toLowerCase()) !== -1);
break;
case "regex":
next = q.shift(); // the regex to match
stack.push(next.test(item.toString()));
break;
case "==":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 == operand2);
break;
case "!=":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 != operand2);
break;
case "<":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 < operand2);
break;
case "<=":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 <= operand2);
break;
case ">":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 > operand2);
break;
case ">=":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 >= operand2);
break;
case "++":
next = q.shift();
if (next == "*") {
stack.push(item.projects ? true : false);
} else if (next.startsWith('"')) {
stack.push(item.projects && item.projects.includes(next.slice(1,-1)));
} else {
// match for next as a substring of the project name
stack.push(item.projects && item.projects.findIndex(function(p) {
return p.indexOf(next) > -1;
}) > -1);
}
break;
case "@@":
next = q.shift();
if (next == "*") {
stack.push(item.contexts ? true : false);
} else if (next.startsWith('"')) {
stack.push(item.contexts && item.contexts.includes(next.slice(1,-1)));
} else {
// match for next as a substring of the context name
stack.push(item.contexts && item.contexts.findIndex(function(c) {
return c.indexOf(next) > -1;
}) > -1);
}
break;
case "||":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 || operand2);
break;
case "&&":
operand2 = stack.pop();
operand1 = stack.pop();
stack.push(operand1 && operand2);
break;
case "!!":
operand1 = stack.pop();
stack.push(!operand1);
break;
default:
// should be a data item like a string or date in millisec, ...
stack.push(opcode);
break;
}
}
return stack.pop();
}
export { runQuery };
"use strict";
import { userData, handleError, translations, setUserData, startBuilding, getConfirmation } from "../render.js";
import { createModalJail } from "../configs/modal.config.mjs";
import { _paq } from "./matomo.mjs";
import { items } from "./todos.mjs";
import { isToday, isPast, isFuture } from "./date.mjs";
import * as filterlang from "./filterlang.mjs";
import { runQuery } from "./filterquery.mjs";
const todoTableSearch = document.getElementById("todoTableSearch");
const autoCompleteContainer = document.getElementById("autoCompleteContainer");
......@@ -17,6 +20,8 @@ let categories,
filtersCounted,
filtersCountedReduced,
selectedFilters,
lastFilterQueryString = null,
lastFilterItems = null,
container,
headline;
......@@ -115,25 +120,36 @@ function filterItems(items) {
});
});
}
if (todoTableSearch.value && todoTableSearch.value.startsWith("?")) {
// if search starts with "?", parse it with filter query language grammar
if (todoTableSearch.value) { // assume that this is an advanced search expr
let queryString = todoTableSearch.value;
try {
let query = filterlang.parse(todoTableSearch.value.slice(1));
let query = filterlang.parse(queryString);
if (query.length > 0) {
items = items.filter(function(item) {
return runQuery(item, query);
});
lastFilterQueryString = queryString;
lastFilterItems = items;
}
} catch(e) {
// if query is malformed, don't match anything, so user can tell that
// query is busted.
items = [];
// oops, that wasn't a syntactically correct search expression
if (lastFilterQueryString && queryString.startsWith(lastFilterQueryString)) {
// keep table more stable by using the previous valid query while
// user continues to type additional query syntax.
items = lastFilterItems;
} else {
// the query is not syntactically correct and isn't a longer version
// of the last working query, so let's assume that it is a
// plain-text query.
items = items.filter(function(item) {
return item.toString().toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
});
}
}
}
// apply filters or filter by search string
// apply filters
items = items.filter(function(item) {
if(!item.text) return false
if(todoTableSearch.value && !todoTableSearch.value.startsWith("?") && item.toString().toLowerCase().indexOf(todoTableSearch.value.toLowerCase()) === -1) return false;
if(!userData.showHidden && item.h) return false;
if(!userData.showCompleted && item.complete) return false;
if(!userData.showDueIsToday && item.due && isToday(item.due)) return false;
......@@ -366,12 +382,10 @@ function generateFilterButtons(category, autoCompleteValue, autoCompletePrefix,
for (let filter in filtersCounted) {
// skip this loop if no filters are present
if(!filter) continue;
let todoFiltersItem = document.createElement("a");
todoFiltersItem.setAttribute("class", "button");
let todoFiltersItem = document.createElement("button");
if(category==="priority") todoFiltersItem.classList.add(filter);
todoFiltersItem.setAttribute("data-filter", filter);
todoFiltersItem.setAttribute("data-category", category);
todoFiltersItem.setAttribute("href", "#");
if(autoCompletePrefix===undefined) { todoFiltersItem.setAttribute("tabindex", 0) } else { todoFiltersItem.setAttribute("tabindex", 0) }
todoFiltersItem.innerHTML = filter;
if(autoCompletePrefix==undefined) {
......@@ -381,6 +395,9 @@ function generateFilterButtons(category, autoCompleteValue, autoCompletePrefix,
});
// add context menu
todoFiltersItem.addEventListener("contextmenu", event => {
// jail the modal
createModalJail(filterContext);
filterContext.style.left = event.x + "px";
filterContext.style.top = event.y + "px";
filterContext.classList.add("is-active");
......@@ -432,14 +449,17 @@ function generateFilterButtons(category, autoCompleteValue, autoCompletePrefix,
// autocomplete container
} else {
// add filter to input
todoFiltersItem.addEventListener("click", () => {
if(autoCompleteValue) {
todoFiltersItem.addEventListener("click", (event) => {
if(autoCompletePrefix && autoCompleteValue) {
// remove composed filter first, then add selected filter
document.getElementById("modalFormInput").value = document.getElementById("modalFormInput").value.replace(autoCompletePrefix + autoCompleteValue, autoCompletePrefix + todoFiltersItem.getAttribute("data-filter") + " ");
} else {
} else if(autoCompletePrefix) {
// add button data value to the exact caret position
document.getElementById("modalFormInput").value = [document.getElementById("modalFormInput").value.slice(0, caretPosition), todoFiltersItem.getAttribute('data-filter'), document.getElementById("modalFormInput").value.slice(caretPosition)].join('') + " ";
}
// empty autoCompleteValue to prevent multiple inputs using multiple Enter presses