notes/main.js

2927 lines
57 KiB
JavaScript

var defaultsettings =
{
fontsize: "16px",
lineheight: "24px",
margins: "20%",
fontfamily: "helvetica,system-ui",
savedelay: 2000,
defaultpreviewinsplit: false,
tagautocomplete: false,
enablenetwork: true,
titlebydefault: false,
linksinnewtab: true,
colors: true,
password: "",
sync: false,
tagsinlists: true,
uselinkpopup: true,
autosorttodo: true
};
//builtin
const markerslist = ["* ", "- ", " * ", " - ", ">> ", "> ", "=> ", "— ", "[ ] ", " ", "• ", "- [ ]", "[x] ", "- [x]"];
const codelanguages = ["xml", "js", "sql"];
const tagmark = "+";
// globals
var previoustitle = "";
var metadata = null;
var fileindex = 0;
var workerid = null;
var backup = "";
var settings = null;
var tags = null;
var pending = {};
var commands = [
{
hint: "Close menu"
},
{
shortcut: "ctrl+shift+P",
hint: "Command palette",
action: commandpalette,
},
{
shortcut: "ctrl+p",
hint: "Show notes list",
action: searchandloadnote
},
{
shortcut: "ctrl+n",
hint: "New note",
action: startnewnote
},
{
shortcut: "ctrl+shift+N",
hint: "Quick new note",
action: quicknewnote
},
{
hint: "Force save",
action: flush,
shortcut: "ctrl+s"
},
{
hint: "Share note",
action: share
},
{
hint: "Share note (html)",
action: sharehtml
},
{
shortcut: "ctrl+g",
hint: "Find in notes",
action: showgrep
},
{
shortcut: "ctrl+i",
hint: "Toggle title",
action: toggletitle
},
{
shortcut: "ctrl+m",
hint: "Toggle preview",
action: togglepreview
},
{
shortcut: "ctrl+shift+M",
hint: "Toggle preview with merged subnotes",
action: togglepreviewwithsubs
},
{
shortcut: "ctrl+d",
hint: "Delete note",
action: deletecurrentnote
},
{
hint: "Restore current note",
action: restore
},
{
hint: "Insert markdown header",
action: insertheader
},
{
hint: "Show help",
action: showhelp
},
{
hint: "Search tags",
action: searchtags,
shortcut: "ctrl+shift+T"
},
{
hint: "Toggle split view",
action: togglesplit
},
{
hint: "Load previous note",
action: loadprevious,
shortcut: "alt+ArrowLeft"
},
{
hint: "Load next note",
action: loadnext,
shortcut: "alt+ArrowRight"
},
{
hint: "Settings",
action: editsettings
},
{
hint: "Change a setting",
action: changesetting
},
{
hint: "Restore default settings",
action: restoresettings
},
{
hint: "Note outline",
action: showoutline,
shortcut: "ctrl+o"
},
{
hint: "Show connected notes",
action: shownotelinks,
shortcut: "ctrl+l"
},
{
hint: "Show stats",
action: showinfo,
shortcut: "ctrl+w"
},
{
hint: "Toggle spell check",
action: togglespellcheck,
shortcut: "F7"
},
{
hint: "Create subnote from selection",
action: createsubnote
},
{
hint: "Merge subnote",
action: includesub
},
{
hint: "Comment selection",
action: comment
},
{
hint: "Download current note",
action: downloadnote
},
{
hint: "Download current note with merged subnotes",
action: downloadnotewithsubs
},
{
hint: "Download all notes (md files in zip archive)",
action: downloadnotes,
shortcut: "ctrl+shift+S"
},
{
hint: "Download all notes (html files in zip archive)",
action: downloadhtmlnotes
},
{
hint: "Insert text in todo",
action: promptinserttodo
},
{
hint: "Edit pgp keys",
action: editpgpkeys
},
{
hint: "Decrypt note",
action: decryptnote
},
{
hint: "Restore deleted note",
action: restoredeleted
},
{
hint: "Replace",
shortcut: "ctrl+h",
action: searchandreplace
},
{
hint: "Sort todo.txt list",
action: sortcurrentastodo
},
{
hint: "Sort text",
action: sortselection
},
{
hint: "Show backlinks",
action: backlinks
},
{
hint: "Remove completed tasks",
action: purgetodo
}];
var snippets = [
{
command: "/code",
hint: "Code block",
insert: "```\n\n```",
cursor: -4
},
{
command: "/date",
hint: "Current date",
insert: (new Date).toISOString().substring(0, 10),
cursor: 0
},
{
command: "/bonjour",
hint: "Standard answer (fr)",
insert: "Bonjour ,\n\n\n\nBien cordialement,\nSimon",
cursor: -29
},
{
command: "/hello",
hint: "Standard answer (en)",
insert: "Hello ,\n\n\n\nKind regards,\nSimon",
cursor: -24
},
{
command: "/-",
hint: "Dialog mark",
insert: "— ",
cursor: 0
},
{
command: "/comment",
hint: "Comment",
insert: "<!--\n\n-->",
cursor: -4
},
{
command: "/x",
hint: "Mark todo entry done",
insert: "x " + (new Date).toISOString().substring(0, 10) + " "
}];
function genguid()
{
return crypto.randomUUID();
}
function purgetodo()
{
if (currentistodo() && confirm("Remove completed tasks?"))
{
seteditorcontent(md.value.replace(/\nx .*/g, ""));
}
}
function seteditorcontent(content)
{
md.value = content;
applycolors();
flush();
resize();
}
function encryptstring(str)
{
var key = localStorage.getItem("pgpkeys").split("-----BEGIN PGP PRIVATE KEY BLOCK-----")[0];
var publicKey = null;
return openpgp.readKey({ armoredKey: key })
.then(res =>
{
publicKey = res;
return openpgp.createMessage({ text: str });
})
.then(message =>
{
return openpgp.encrypt({
message: message,
encryptionKeys: publicKey });
});
}
function decryptstring(str)
{
if (!str.startsWith("-----BEGIN PGP MESSAGE-----"))
{
// console.log(str + ": string is not encrypted");
return Promise.resolve(str);
}
var key = localStorage.getItem("pgpkeys").split("-----END PGP PUBLIC KEY BLOCK-----")[1];
var privateKey = null;
return openpgp.readKey({ armoredKey: key })
.then(res =>
{
privateKey = res;
return openpgp.readMessage({ armoredMessage: str })
})
.then(message =>
{
return openpgp.decrypt({
message: message,
decryptionKeys: privateKey });
})
.then(decrypted =>
{
const chunks = [];
for (const chunk of decrypted.data) {
chunks.push(chunk);
}
return chunks.join('');
})
}
function getrangecontent(range)
{
return md.value.substring(range.start, range.end);
}
function currentrange()
{
return {
start: md.selectionStart,
end: md.selectionEnd
};
}
function pushitem(key, value)
{
unsavedmark.hidden = false;
pending[key] = true;
var name = metadata && metadata[key] ? metadata[key].title : key;
queryremote({action: "push", name: key, data: value})
.then( () =>
{
console.log("'" + name + "' pushed to server");
delete pending[key];
})
.catch( err =>
{
console.error("error while pushing '" + name + "': " + err);
showtemporaryinfo(msg);
setTimeout( () =>
{
serialize(key, value);
}, settings.savedelay);
})
.finally( () =>
{
unsavedmark.hidden = Object.keys(pending) == 0;
});
}
function serialize(key, value)
{
localStorage.setItem(key, value);
var name = metadata && metadata[key] ? metadata[key].title : key;
console.log("'" + name + "' serialized locally");
if (settings.sync)
{
pushitem(key, value);
}
}
function createsubnote()
{
var title = prompt("Subnote tite:");
if (!title)
{
showtemporaryinfo("No title provided");
setpos(md.selectionStart);
md.focus();
}
else
{
var guid = getguid(title);
if (guid)
{
showtemporaryinfo("'" + title + "' already exists");
setpos(md.selectionStart);
md.focus();
}
else
{
var range = currentrange();
var content = getrangecontent(range);
guid = createnote(title);
serialize(guid, content);
seteditorcontent(md.value.substring(0, range.start)
+ "[[" + title + "]]"
+ md.value.substring(range.end));
}
}
}
function comment()
{
seteditorcontent(md.value.substring(0, md.selectionStart)
+ "<!-- "
+ md.value.substring(md.selectionStart, md.selectionEnd)
+ " -->"
+ md.value.substring(md.selectionEnd));
}
function includesub()
{
var range = linkrangeatpos();
if (range)
{
var title = linkatpos();
if (confirm("Replace [[" + title + "]] by its content?"))
{
var subnote = getnote(title);
seteditorcontent(md.value.substring(0, range.start)
+ subnote.content
+ md.value.substring(range.end));
if (confirm("Delete '" + title + "'?"))
{
deletenote(title);
}
}
}
}
function togglespellcheck()
{
md.spellcheck = !md.spellcheck;
}
function formatsize(size)
{
var unit = "b";
if (size > 1024)
{
size /= 1024;
unit = "kb";
}
if (size > 1024)
{
size /= 1024;
unit = "mb";
}
return size.toFixed(2) + " " + unit;
}
function pospercent()
{
return md.value.length > 0 ?(100 * md.selectionStart / md.value.length).toFixed(2) : 100;
}
function showinfo()
{
var tags = gettags(md.value);
showtemporaryinfo(
[
"sync: " + (settings.sync ? "en" : "dis") + "abled",
"pending: " + Object.keys(pending).join(),
"title: " + title.value,
"line count: " + md.value.split("\n").length,
"word count: " + getwords(),
"cursor position: " + md.selectionStart + " (" + pospercent() + "%)",
(tags ? "tags: " + tags : ""),
"notes count: " + sortedlist().length,
"spell check: " + (md.spellcheck ? "en" : "dis") + "abled"
].join("\n"));
}
function savesettings()
{
localStorage.setItem("settings", JSON.stringify(settings));
}
function descendants(note)
{
var list = [note];
var result = [];
while (list.length)
{
var current = list.shift();
if (result.indexOf(current) == -1)
{
result.push(current);
list = list.concat(children(current));
}
}
return result;
}
function children(guid)
{
var content = localStorage.getItem(guid);
return (content
.match(/\[\[([^\]]*)\]\]/g) || [])
.map(l => l.replace("[[", "").replace("]]", ""))
.filter(l => !l.includes("(deleted)"))
.map(l => getguid(l));
}
function parents(guid)
{
return Object.keys(metadata)
.filter(g => localStorage.getItem(g).indexOf("[[" + metadata[guid].title + "]]") != -1);
}
function connected(guid)
{
var list = [guid];
var result = [];
while (list.length)
{
var current = list.shift();
if (result.indexOf(current) == -1)
{
result.push(current);
list = list.concat(children(current)).concat(parents(current));
}
}
return result;
}
function toggleeditor(hidden)
{
md.hidden = hidden;
if (settings.colors)
{
colored.hidden = hidden;
}
}
function shownotelinks()
{
if (settings.enablenetwork)
{
networkpage.hidden = false;
toggleeditor(true);
var nodes = [];
var edges = [];
var list = [getguid(title.value)];
while (list.length)
{
var current = list.shift();
if (!nodes.find(n => n.id == current))
{
nodes.push(
{
id: current,
label: metadata[current].title
});
var buddies = children(current).concat(parents(current));
list = list.concat(buddies);
buddies.
forEach(buddy => {
if (!edges.find(edge => (edge.to == current && edge.from == buddy) || (edge.to == buddy && edge.from == current)))
{
edges.push({
from: current,
to: buddy
});
}
});
}
}
var data = {
nodes: nodes,
edges: edges
};
var options =
{
nodes:
{
color:
{
background: "white",
border: "black",
},
font:
{
color: "black",
size: 16
}
}
};
var graph = new vis.Network(network, data, options);
graph.on("click", function(event)
{
networkpage.hidden = true;
toggleeditor(false);
loadnote(nodes.find(n => n.id == event.nodes[0]).label);
});
graph.setSelection(
{
nodes : [getguid(title.value)]
});
}
else
{
searchinlist(connected(getguid(title.value)).map(g => metadata[g].title))
.then(loadnote);
}
}
function showoutline()
{
var outline = {};
var pos = 0;
md.value.split("\n").forEach((line, index, lines) =>
{
pos += line.length + 1;
if (line.startsWith("#"))
{
line = line
.replace("# ", "")
.replace(/#/g, "\xa0".repeat(4));
outline[line] = pos;
}
else if (line == "---" && index != 0 && index != 3)
{
var next = lines.find((current, i) =>
{
return i > index && current != "";
});
if (next)
{
var nbcar = 80;
next = next.length < nbcar ? next : next.substring(0, nbcar) + "...";
outline[next] = pos;
}
}
});
var keys = Object
.keys(outline)
.sort((a,b) =>
{
return outline[a] - outline[b];
});
searchinlist(keys)
.then(line =>
{
md.setSelectionRange(outline[line], outline[line]);
md.focus();
});
}
function linkrangeatpos()
{
var start = md.value.lastIndexOf("[[", md.selectionStart);
if (start == -1 || md.value.substring(start, md.selectionStart).indexOf("\n") != -1) return null
var end = md.value.indexOf("]]", md.selectionStart);
if (end == -1 || md.value.substring(md.selectionStart, end).indexOf("\n") != -1) return null;
return {
start: start,
end: end + 2
};
}
function linkatpos()
{
var range = linkrangeatpos();
if (range)
{
return md.value.substring(range.start + 2, range.end - 2);
}
return null;
}
function tagatpos()
{
if (md.value.lastIndexOf("tags: ", md.selectionStart) < md.value.lastIndexOf("\n", md.selectionStart) || md.selectionStart < 6)
{
return null;
}
var start = md.value.lastIndexOf(" ", md.selectionStart);
if (start == -1 || md.value.substring(start, md.selectionStart).indexOf("\n") != -1) return "";
var eol = md.value.indexOf("\n", md.selectionStart);
var end = md.value.indexOf(",", md.selectionStart);
if (end == -1 || eol < end)
{
end = eol;
}
return md.value.substring(start + 1, end);
}
function removelinkdialog()
{
if (typeof linkdialog != "undefined")
{
notepage.removeChild(linkdialog);
}
}
function showlinkdialog(link)
{
var div = document.createElement("div");
div.setAttribute("style", "top:" + event.pageY + "px;left:" + event.pageX + "px");
div.setAttribute("id", "linkdialog");
var a = document.createElement("a");
a.setAttribute("id", "linkelt");
if (link.startsWith("http"))
{
a.setAttribute("href", link);
a.setAttribute("target", "_blank");
div.onclick = removelinkdialog;
a.innerText = "Open on " +
link.replace("https://", "")
.replace("http://", "")
.replace("www.", "")
.replace(/\/.*/, "");
}
else
{
a.setAttribute("href", "#");
a.innerText = link;
div.onclick = function()
{
removelinkdialog();
loadnote(link);
};
}
div.appendChild(a);
notepage.appendChild(div);
}
function checkatpos(pos)
{
if (pos > 0 && md.value[pos - 1] == "[" && md.value[pos + 1] == "]")
{
return {
pos: pos,
val: md.value[pos]
};
}
return null;
}
function clickedcheck()
{
return checkatpos(md.selectionStart) || checkatpos(md.selectionStart + 1) || checkatpos(md.selectionStart - 1);
}
function clickeditor()
{
var word, link;
if (event.ctrlKey)
{
link = linkatpos();
var tag = tagatpos();
word = wordatpos();
if (link)
{
loadnote(link);
}
else if (tag)
{
tagslist();
searchinlist(tags[tag.toLowerCase()])
.then(loadnote);
}
else if (word.startsWith("http"))
{
window.open(word, '_blank');
}
}
else if (clickedcheck())
{
var res = clickedcheck();
seteditorcontent(md.value.substring(0, res.pos)
+ (res.val == " " ? "x" : " ")
+ md.value.substring(res.pos + 1));
setpos(res.pos);
}
else if (settings.uselinkpopup)
{
removelinkdialog();
link = linkatpos();
if (link)
{
showlinkdialog(link);
}
else
{
word = wordatpos();
if (word.startsWith("http"))
{
showlinkdialog(word);
}
}
}
}
function restoresettings()
{
if (confirm("Restore default settings?"))
{
settings = defaultsettings;
savesettings();
loadsettings();
}
}
function editsettings()
{
bind("settings.json", JSON.stringify(settings, null, " "));
}
function editsetting(name)
{
if (typeof settings[name] != "undefined")
{
var value = settings[name];
var type = typeof value;
if (type != "undefined")
{
value = prompt(name, value);
if (value !== null)
{
if (type == "number")
{
value = parseInt(value);
}
else if (type == "boolean")
{
value = value === "true";
}
settings[name] = value;
savesettings();
loadsettings();
}
}
}
}
function changesetting()
{
searchinlist(Object.keys(settings).map(name => name + ": " + settings[name]))
.then(setting =>
{
editsetting(setting.split(":").shift());
});
}
function decryptnote()
{
decryptstring(md.value)
.then(decrypted =>
{
seteditorcontent(decrypted);
resize();
});
}
function editpgpkeys()
{
bind("pgpkeys", localStorage.getItem("pgpkeys") || "");
}
function showtemporaryinfo(info)
{
alert(info);
}
function getwords()
{
return md.value.split(/\s+\b/).length;
}
function issplit()
{
return window.location !== window.parent.location;
}
function togglesplit()
{
if (issplit())
{
window.parent.location = "index.html";
}
else
{
window.location = "split.html";
}
}
function tagslist()
{
tags = {};
Object.values(metadata)
.forEach(n =>
{
var ts = n.header.tags;
ts.forEach(t =>
{
tags[t] = tags[t] || [];
tags[t].push(n.title);
});
});
return searchinlist(Object.keys(tags).sort());
}
function searchtags()
{
tagslist()
.then(tag => searchinlist(tags[tag]))
.then(loadnote);
}
function gettags(content)
{
var i = content.indexOf("tags: ");
if (i > -1)
{
var j = content.indexOf("\n", i);
return content.substring(i + 6, j)
.split(",")
.map(t => t.toLowerCase().trim())
.filter(t => t.length);
}
return [];
}
function share()
{
if (navigator.share)
{
navigator.share(
{
text: md.value,
title: title.value
});
}
}
function sharehtml()
{
if (navigator.share)
{
var file = new File(['<html><body>' + md2html(md.value) + '</body></html>'],
title.value + ".html",
{
type: "text/html",
});
navigator.share(
{
title: title.value,
files: [file]
});
}
}
function getfilename(title)
{
return title.replace(/[\?\"<>|\*:\/\\]/g, "_") + ".md";
}
function download(filename, content)
{
var element = document.createElement('a');
element.setAttribute('href', 'data:text/markdown;charset=utf-8,' + encodeURIComponent(content));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
element = null;
}
function downloadnotes()
{
var zip = new JSZip();
Object.keys(metadata).forEach(guid =>
{
zip.file(getfilename(metadata[guid].title), localStorage.getItem(guid));
});
zip.generateAsync({type:"blob"})
.then(function(content)
{
saveAs(content, "notes-" + timestamp() + ".zip");
});
}
function downloadhtmlnotes()
{
var zip = new JSZip();
Object.keys(metadata).forEach(guid =>
{
zip.file(getfilename(metadata[guid].title).replace(".md", ".html"), md2html(localStorage.getItem(guid)));
});
zip.generateAsync({type:"blob"})
.then(function(content)
{
saveAs(content, "notes-html-" + timestamp() + ".zip");
});
}
function headerandtext(content)
{
var result =
{
header: "",
text: content
};
if (content.startsWith("---\n"))
{
var end = content.indexOf("---\n", 4);
if (end > -1)
{
result.header = content.substring(0, end + 4);
result.text = content.substring(end + 4);
}
}
return result;
}
function inserttodo(text)
{
var guid = getguid("todo");
if (!guid)
{
guid = createnote("todo");
}
var content = localStorage.getItem(guid);
var split = headerandtext(content);
content = split.header + text + "\n" + split.text;
if (title.value == "todo")
{
seteditorcontent(content);
}
else
{
serialize(guid, content);
metadata[guid].lastchanged = Date.now();
flush();
}
}
function promptinserttodo()
{
var text = prompt("Text:");
if (text)
{
inserttodo(text);
}
}
function downloadnotewithsubs()
{
var note = withsubs();
if (note)
{
download(note.title + ".md", note.content);
}
}
function downloadnote()
{
download(title.value + ".md", md.value);
}
function remotecallfailed(error)
{
if (error)
{
console.warn(error);
showtemporaryinfo("Error: " + error);
}
else
{
console.warn("remotecallfailed without details");
}
}
function getguid(title)
{
return Object.keys(metadata).find(guid => metadata[guid].title === title);
}
function gotoline(line)
{
var i = 0;
var pos = 0;
while (i < line && pos > -1)
{
pos = md.value.indexOf("\n", pos + 1);
i++;
}
if (pos > -1)
{
setpos(pos + 1);
}
}
function createnote(title)
{
var guid = genguid();
var content = defaultheaders();
var item = {
lastchanged: Date.now(),
title: title,
pos: content.length,
header: indexheader(content)
};
metadata[guid] = item;
serializeindex()
serialize(guid, content);
return guid;
}
function loadstorage()
{
metadata = JSON.parse(localStorage.getItem("index"));
if (!metadata)
{
metadata = {};
createnote(timestamp());
}
var params = new URLSearchParams(window.location.search);
var clip = params.get("c");
if (clip)
{
settings.savedelay = 0;
colored.hidden = true;
md.hidden = true;
var msg = document.createElement("div");
msg.innerText = "Clipping...";
notepage.appendChild(msg);
inserttodo("@clip " + clip)
window.close();
}
var title = params.get("n") || params.get("name");
if (window.title.value)
{
// reload current
loadnote(window.title.value);
}
else if (title)
{
loadnote(title);
var line = params.get("l");
if (line)
{
gotoline(parseInt(line));
}
}
else
{
loadlast();
}
}
function loadsettings()
{
settings = {...defaultsettings};
var item = window.localStorage.getItem("settings");
if (item)
{
item = JSON.parse(item);
for (var key in settings)
{
if (typeof item[key] !== "undefined")
{
settings[key] = item[key];
}
}
}
document.body.style.fontSize = settings.fontsize;
document.body.style.lineHeight = settings.lineheight;
document.body.style.marginLeft = settings.margins;
document.body.style.marginRight = settings.margins;
document.body.style.fontFamily = settings.fontfamily;
if (settings.titlebydefault && title.hidden)
{
toggletitle();
}
colored.hidden = !settings.colors;
md.style.color = settings.colors ? "transparent" : "inherit";
md.style.background = settings.colors ? "transparent" : "inherit";
}
function checksaved()
{
if (!unsavedmark.hidden)
{
return "not saved";
}
}
function initsnippets()
{
// code languages
codelanguages.forEach(lang =>
{
if (!snippets.find(s => s.command == "/" + lang))
{
snippets.push(
{
command: "/" + lang,
hint: lang + " code block",
insert: "```" + lang + "\n\n```",
cursor: -4
});
}
});
}
function indexheader(content)
{
var indexedheader = {
tags: []
};
var hat = headerandtext(content);
var header = hat.header;
if (header)
{
header.split("\n").forEach(line =>
{
if (line && line != "---")
{
var t = line.split(":");
var val = t.pop();
var key = t.pop();
if (key == "tags")
{
val = val.split(",").map(t => t.trim());
if (val.length == 1 && !val[0])
{
val.pop();
}
}
indexedheader[key] = val;
}
});
}
return indexedheader;
}
function migratelegacystorage()
{
var legacy = localStorage.getItem("data");
if (legacy)
{
alert("Hey! I am about to migrate your notes to the brand new data model. No worries, I will keep a backup somewhere in case things go wrong. Take a deep breath, and click ok when you're ready.");
localStorage.setItem("legacy", legacy);
legacy = JSON.parse(legacy);
var index = {};
legacy.reverse().forEach( (note, i) =>
{
var guid = genguid();
localStorage.setItem(guid, note.content);
note.header = indexheader(note.content);
note.lastchanged = i;
delete note.content;
index[guid] = note;
});
localStorage.setItem("index", JSON.stringify(index));
localStorage.removeItem("data");
}
}
function pushall()
{
var index = JSON.parse(localStorage.getItem("index"));
var list = [queryremote({action: "push", name: "index", data: localStorage.getItem("index")})];
Object.keys(index).forEach(guid =>
{
list.push(queryremote({action: "push", name: guid, data: localStorage.getItem(guid)}));
});
return Promise.all(list);
}
function fetchandserialize(guid)
{
var name = (metadata && metadata[guid]) ? metadata[guid] : guid;
console.log("fetching '" + name + "'");
return queryremote({action: "fetch", name: guid})
.then(content =>
{
localStorage.setItem(guid, content);
console.log("'" + name + "' fetched and serialized");
});
}
function fetch()
{
return new Promise(function(resolve)
{
if (settings.sync)
{
var pgpkeys = localStorage.getItem("pgpkeys");
if (!pgpkeys)
{
loadstorage();
editpgpkeys();
showtemporaryinfo("Pgp key empty or invalid. Enter PGP keys and refresh.");
}
else
{
queryremote({action: "fetch", name: "index"})
.then(filecontent =>
{
if (!filecontent)
{
if (confirm("No remote index found. Init remote now?"))
{
pushall().then(resolve);
}
else
{
resolve();
}
}
else
{
var localindex = JSON.parse(localStorage.getItem("index"));
var remoteindex = JSON.parse(filecontent);
var list = [];
Object.keys(remoteindex).forEach(guid =>
{
if (!localindex[guid] || localindex[guid].lastchanged < remoteindex[guid].lastchanged)
{
list.push(fetchandserialize(guid));
}
});
Promise.all(list).then( () =>
{
localStorage.setItem("index", JSON.stringify(remoteindex));
resolve();
});
}
})
.catch(err =>
{
if (err == "error: authent")
{
settings.password = prompt("Password: ", settings.password);
savesettings();
init();
}
else
{
showtemporaryinfo(err);
}
});
}
}
else
{
resolve();
}
});
}
function init()
{
migratelegacystorage();
loadsettings();
window.onbeforeunload = checksaved;
window.onclick = focuseditor;
title.value = "";
initsnippets();
fetch()
.then( () =>
{
loadstorage();
if (issplit())
{
if (settings.defaultpreviewinsplit && name == "right")
{
togglepreview();
}
else if (name == "left")
{
md.focus();
}
}
});
}
function encryptdata(params)
{
if (params.data)
{
return encryptstring(params.data)
.then(encrypted =>
{
params.data = encrypted;
return Promise.resolve(params);
});
}
else
{
return Promise.resolve(params);
}
}
function queryremote(params)
{
return encryptdata(params)
.then(encparams =>
{
return new Promise ( (resolve, reject) =>
{
encparams.password = settings.password;
var paramlist = [];
for (var i in encparams)
{
paramlist.push(i + "=" + encodeURIComponent(encparams[i]));
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "handler.php");
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.onerror = function()
{
reject("XMLHttpRequest error");
}
xhr.onload = function()
{
if (xhr.status !== 200)
{
reject("Http status " + xhr.status);
}
else
{
decryptstring(xhr.responseText)
.then(decrypted =>
{
if (decrypted.startsWith("error: "))
{
reject(decrypted);
}
else
{
resolve(decrypted);
}
});
}
}
var paramstring = paramlist.join("&");
console.log("http '" + encparams.action + "' request length: " + formatsize(paramstring.length));
xhr.send(paramstring);
});
});
}
function getlinesrange()
{
var start = md.selectionStart;
var end = md.selectionEnd;
while (start > 0 && md.value[start - 1] != "\n") start--;
while (end < md.value.length && md.value[end] != "\n") end++;
return {
start: start,
end: end
};
}
function backlinks()
{
searchinlist(Object.keys(metadata)
.filter(guid => localStorage.getItem(guid).includes("[[" + title.value + "]]"))
.map(guid => metadata[guid].title))
.then(loadnote);
}
function sortselection()
{
var content = md.value;
var range = {start: 0, end: content.length};
if (md.selectionStart != md.selectionEnd)
{
range = getlinesrange();
}
var selection = content.substring(range.start, range.end);
var sorted = selection.split("\n").sort().join("\n");
seteditorcontent(content.substring(0, range.start) + sorted + content.substring(range.end));
}
function wordatpos()
{
var words = md.value.split(/\s/);
var i = 0;
var word = "";
while (i < md.selectionStart)
{
word = words.shift();
i += word.length + 1;
}
return word;
}
function ontopbarclick()
{
if (title.hidden)
{
commandpalette();
}
}
function md2html(content)
{
var converter = new showdown.Converter();
converter.setOption("simplifiedAutoLink", true);
converter.setOption("simpleLineBreaks", true);
converter.setOption("metadata", true);
converter.setOption("tasklists", true);
converter.setOption("literalMidWordUnderscores", true);
if (settings.linksinnewtab)
{
converter.setOption("openLinksInNewWindow", true);
}
var html = converter.makeHtml(content);
// internal links
html = html.replace(/\[\[([^\]]*)\]\]/g, "<a href='#' onclick='loadnote(\"$1\");'>$1</a>");
return html;
}
function loadlast()
{
loadnote(sortedlist()[0].title);
}
function loadprevious()
{
var list = sortedlist();
var index = list.findIndex(i => i.title == title.value);
if (index > -1 && index < list.length - 1)
{
loadnote(list[index + 1].title);
}
}
function loadnext()
{
var list = sortedlist();
var index = list.findIndex(i => i.title == title.value);
if (index > -1 && index > 0)
{
loadnote(list[index - 1].title);
}
}
function sortedlist(deleted)
{
return Object
.values(metadata)
.filter(i =>
{
return deleted === true ? i.deleted : !i.deleted;
})
.sort( (i,j) => j.lastchanged - i.lastchanged);
}
function grep(needle)
{
var result = {};
sortedlist()
.forEach(item =>
{
if (item.title.toLowerCase().includes(needle.toLowerCase()))
{
result[item.title] = {};
}
var content = localStorage.getItem(getguid(item.title));
content.split("\n")
.forEach((line, nb) => {
if (line.toLowerCase().includes(needle.toLowerCase()))
{
result[item.title] = result[item.title] || {};
result[item.title][nb] = line;
}
});
});
return result;
}
function showgrepresult(needle, grepresult)
{
var grepcontent = ["# Search results: \"" + needle + "\""];
for (var file in grepresult)
{
grepcontent.push("[[" + file + "]]");
for (var l in grepresult[file])
{
grepcontent.push("[<a href=?n=" + encodeURIComponent(file) + "&l=" + l + ">" + l + "</a>] " + grepresult[file][l]);
}
grepcontent.push("");
}
if (grepcontent.length == 0)
{
grepcontent.push("No result.");
}
bind("Search result", grepcontent.join("\n"));
if (preview.hidden)
{
togglepreview();
}
}
function showgrep()
{
var text = prompt("Search:", md.selectionEnd > md.selectionStart ? md.value.substr(md.selectionStart, md.selectionEnd - md.selectionStart).trim() : "");
if (text)
{
showgrepresult(text, grep(text));
}
}
function commandpalette()
{
searchinlist(commands
.filter(command => !command.excludepalette)
.map(command =>
{
return {
prefix: "command ",
text: command.hint,
suffix: command.shortcut ? [command.shortcut.toLowerCase()] : null
};
})
.concat(snippets.map(s =>
{
return {
prefix: "snippet ",
text: s.hint,
suffix: [s.command]
};
}))
.concat(
sortedlist()
.map(item =>
{
return {
prefix: "note ",
text: item.title,
suffix: item.header.tags.map(t => tagmark + t)
};
}))
.concat(Object.keys(settings).map(s =>
{
return {
prefix: "setting ",
text: s,
suffix: s == "password" ? null : [settings[s]]
};
})))
.then(selected =>
{
if (selected.prefix == "snippet ")
{
var snippet = snippets.find(s => s.hint == selected.text);
insert(snippet.insert, snippet.cursor);
md.focus();
}
else if (selected.prefix == "note ")
{
loadnote(selected.text);
}
else if (selected.prefix == "setting ")
{
editsetting(selected.text);
}
else
{
var command = commands.find(c => c.hint == selected.text);
if (command)
{
executecommand(command);
}
else
{
// if unknown command, create a new note
loadnote(selected);
}
}
});
}
function insert(text, cursoroffset = 0, nbtodelete = 0)
{
var pos = md.selectionStart;
var content = md.value;
seteditorcontent(
content.substring(0, pos - nbtodelete)
+ text
+ content.substring(pos));
setpos(pos - nbtodelete + text.length + cursoroffset);
}
function searchinlist(list)
{
return new Promise(selectitem =>
{
fileindex = 0;
searchdialog.hidden = false;
filteredlist.hidden = false;
filteredlist.innerHTML = "";
filter.value = "";
list.forEach(item =>
{
var elt = document.createElement("div");
elt.tag = item;
if (typeof item === "string")
{
elt.textContent = item;
}
else
{
var ts = document.createElement("span");
ts.setAttribute("class", "searchlistprefix");
ts.innerHTML = item.prefix || "";
elt.appendChild(ts);
ts = document.createElement("span");
ts.innerHTML = item.text;
elt.appendChild(ts);
if (item.suffix)
{
item.suffix.forEach(t =>
{
ts = document.createElement("span");
ts.setAttribute("class", "searchlistsuffix");
ts.innerHTML = "&nbsp;" + t;
elt.appendChild(ts);
});
}
}
elt.onclick = function()
{
searchdialog.hidden = true;
selectitem(elt.tag);
}
filteredlist.appendChild(elt);
});
applyfilter();
filter.onkeydown = function()
{
// doesn't work if focus is lost.
if (event.key === "Enter")
{
event.preventDefault();
searchdialog.hidden = true;
var selected = document.getElementsByClassName("selected")[0];
selectitem(selected ? selected.tag : filter.value);
}
}
filter.focus();
});
}
function applyfileindex()
{
var i = 0;
[...filteredlist.children].forEach(child =>
{
if (child.nodeName == "DIV")
{
child.className = "searchitem";
if(!child.hidden)
{
if (i++ == fileindex)
{
child.className = "selected";
if (child.customevent)
{
child.customevent(child.textContent);
filter.focus();
}
}
}
}
});
}
function getpos()
{
return md.selectionStart;
}
function setpos(pos)
{
md.setSelectionRange(pos, pos);
}
function before(nb)
{
return md.value.substring(getpos() - nb, getpos());
}
function resize()
{
if (md.clientHeight < md.scrollHeight)
{
md.style.height = md.scrollHeight + 'px';
}
}
function postpone()
{
return new Promise(function(resolve)
{
clearTimeout(workerid);
workerid = setTimeout(resolve, settings.savedelay);
});
}
function flush()
{
clearTimeout(workerid);
workerid = null;
var guid = getguid(title.value);
if (guid)
{
var item = metadata[guid];
item.title = title.value;
item.pos = md.selectionStart;
item.header = indexheader(md.value);
item.lastchanged = Date.now();
serializeindex();
serialize(guid, md.value);
}
else if (title.value == "settings.json")
{
settings = JSON.parse(md.value);
savesettings();
loadsettings();
}
else if (title.value == "pgpkeys")
{
localStorage.setItem("pgpkeys", md.value);
}
unsavedmark.hidden = Object.keys(pending).length == 0;
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
var languagekeywords = {
"sql": ["select", "from", "where", "and", "or"],
"js": ["var", "for", "if", "else"],
"zsh": ["sudo"]
}
function currentline()
{
return (md.value.substring(0, md.selectionStart).match(/\n/g) || []).length;
}
function lineatpos(pos)
{
return (md.value.substring(0, pos).match(/\n/g) || []).length;
}
function currentcol()
{
return md.selectionStart - Math.max(0, md.value.lastIndexOf("\n", md.selectionStart - 1)) - 1;
}
function rawline(index)
{
return md.value.split("\n")[index];
}
var emptyline = "<br>";
function rawline2html(line, index, options)
{
line = escapeHtml(line);
// headings
if (line.startsWith("#"))
{
line = line.replace(/(#* )/, "<span class='color-heading-mark'>$1</span>");
line = "<span class='color-heading'>" + line + "</span>";
}
// bold and italics
var temp = line;
if (line.startsWith("* "))
{
temp = line.substring(2);
}
temp = temp.replace(/\*\*([^\*]*)\*\*/g, "<span class='color-bold'>&#42;&#42;$1&#42;&#42;</span>");
temp = temp.replace(/\*([^\*]*)\*/g, "<span class='color-emphasis'>&#42;$1&#42;</span>");
if (line.startsWith("* "))
{
line = "* " + temp;
}
else
{
line = temp;
}
// lists
markerslist.forEach(marker =>
{
if (line.startsWith(marker) && marker.trim())
{
line = line.replace(marker, "<span class='color-list-marker'>" + marker + "</span>");
}
});
// md header
if (index == 0 && line == "---")
{
options.header = true;
}
if (options.header)
{
if (index > 0 && line == "---")
{
options.header = false;
}
line = line || emptyline;
line = "<span class='color-header'>" + line + "</span>";
}
// code blocks
if (line.startsWith("```") && !options.code)
{
options.code = true;
options.language = line.substring(3);
line = "<div class='color-code'>" + line.replace(new RegExp("(" + options.language + ")"), "<span class='color-code-language'>$1</span>") + "</div>";
}
else if (line == "```" && options.code)
{
options.code = false;
options.language = "";
line = "<div class='color-code'>" + line + "</div>";
}
else if (options.code)
{
var comment = false;
if (line.match(/^\s*\/\//))
{
line = "<span class='color-code-comment'>" + line + "</span>";
comment = true;
}
line = "<div class='color-code'>" + (line || emptyline) + "</div>";
if (!comment && languagekeywords[options.language])
{
var keywords = languagekeywords[options.language];
keywords.forEach(keyword =>
{
line = line.replace(new RegExp("\\b(" + keyword + ")\\b", "ig"), "<span class='color-code-keyword'>$1</span>");
});
}
}
// internal links
line = line.replace(/\[\[(.*)\]\]/g, "[[<span class='color-link'>$1</span>]]");
// comments
line = line.replace(/&lt;\!--(.*)/g, "<span class='color-comment'>&lt;!--$1</span>");
if (line.includes("&lt;!--") && !line.includes("--&gt;"))
{
options.comment = true;
}
else if (options.comment)
{
line = line || emptyline;
line = "<span class='color-comment'>" + line
if (line.includes("--&gt;"))
{
options.comment = false;
}
else
{
line += "</span>";
}
}
line = line.replace(/\-\-&gt;/g, "--&gt;</span>");
if (line.startsWith("// "))
{
line = "<span class='color-comment'>" + line + "</span>";
}
// autocomplete snippets
if (index == currentline())
{
var raw = rawline(index);
var pos = currentcol();
var slashpos = raw.substring(0, pos).lastIndexOf("/");
if (slashpos > -1)
{
var spacepos = raw.substring(0, pos).lastIndexOf(" ");
if (slashpos > spacepos)
{
var snippetpart = raw.substring(slashpos);
var matching = snippets
.filter(s => s.command.startsWith(snippetpart))
.map(s => s.command.substring(1));
if (matching.length)
{
line += "<span class='color-autocomplete'>";
line += matching.join().substr(pos - slashpos - 1);
line += "</span>";
}
}
}
}
// todotxt
if (currentistodo())
{
if (line.startsWith("x "))
{
line = "<span class='color-todo-complete'>" + line + "</span>";
}
else
{
line = line.replace(/(\(\w\))/g, "<span class='color-todo-priority'>$1</span>");
line = line.replace(/(@\w*)/g, "<span class='color-todo-project'>$1</span>");
line = line.replace(/(\s\+\w*)/g, "<span class='color-todo-context'>$1</span>");
}
}
// inline code
line = line.replace(/`(.*)`/, "<span class='color-code'>`$1`</span>");
// links
line = line.replace(/(http[^\s]*)/, "<span class='color-link'>$1</span>");
return line;
}
function applycolors(currentonly)
{
if (!settings.colors)
{
return;
}
var options =
{
header: false,
code: false,
comment: false,
language: ""
};
var linediv = null;
if (currentonly)
{
var index = currentline();
linediv = document.getElementById("line" + index);
options = JSON.parse(linediv.getAttribute("tag"));
var line = rawline(index);
line = rawline2html(line, index, options);
linediv.innerHTML = line || emptyline;
}
else
{
console.log("redrawing all colored div");
var lines = md.value.split("\n");
var i = 0;
for (; i < lines.length; i++)
{
linediv = document.getElementById("line" + i);
if (!linediv)
{
linediv = document.createElement("div");
colored.appendChild(linediv);
}
linediv.setAttribute("id", "line" + i);
linediv.setAttribute("tag", JSON.stringify(options));
linediv.innerHTML = rawline2html(lines[i], i, options) || emptyline;
}
// remove remanining
linediv = document.getElementById("line" + (i++));
while (linediv)
{
colored.removeChild(linediv);
linediv = document.getElementById("line" + (i++));
}
}
}
function editorinput()
{
unsavedmark.hidden = false;
// criteria to improve. Or redraw only after?
var multiline = md.value.substring(md.selectionStart, md.selectionEnd).includes("\n");
applycolors(!multiline && event.data && (event.inputType == "insertText" || event.inputType == "deleteContentBackward" || event.inputType == "deleteContentForward"));
// todo: fix if current note change during postponing, the wrong one will be saved. Or prevent binding another note?
postpone().then(flush);
resize();
}
function timestamp()
{
var utc = new Date();
var loc = new Date(utc - utc.getTimezoneOffset() * 60 * 1000);
return loc.toISOString().replace("T", " ").replace(/\..*/, "").replace(/:/g, ".");
}
function quicknewnote()
{
flush();
loadnote(timestamp());
}
function startnewnote()
{
flush();
var title = prompt("Note title: ", timestamp());
if (title)
{
loadnote(title);
}
}
function showhelp()
{
var help = ["# Notes"];
help.push("## Shortcuts");
commands
.filter(command => Boolean(command.shortcut))
.forEach(command => help.push(command.hint + ": " + command.shortcut.toLowerCase()));
help.push("## Snippets");
snippets.forEach(snippet =>
{
help.push(snippet.hint + ": " + snippet.command);
});
help.push("## Libs");
help.push("[Showdown](https://showdownjs.com/)");
help.push("[vis-network](https://visjs.org/)");
help.push("[openpgpjs](https://openpgpjs.org/)");
help.push("[jszip](https://stuk.github.io/jszip/)");
help.push("[FileSaver](http://eligrey.com)");
help.push("## Inspiration");
help.push("[rwtxt](https://rwtxt.com)");
help.push("[Offline Notepad](https://offlinenotepad.com/)");
help.push("[Writemonkey3](http://writemonkey.com/wm3/)");
help.push("[Sublime Text](https://www.sublimetext.com/)");
help.push("[Notion](https://www.notion.so/)");
help.push("[Calmly Writer](https://calmlywriter.com/)");
help.push("[Cryptee](https://crypt.ee/)");
help.push("##Sources");
help.push("https://github.com/quenousimporte/notes");
bind("Help", help.join("\n"));
if (preview.hidden)
{
togglepreview();
}
}
function toggletitle()
{
if (title.hidden)
{
title.hidden = false;
title.focus();
}
else
{
title.hidden = true;
md.focus();
}
}
function selectnote()
{
return searchinlist(
sortedlist()
.map(item =>
{
return {
text: item.title,
suffix: item.header.tags.map(t => tagmark + t)
}
}));
}
function searchautocomplete()
{
selectnote().then(selected =>
{
insertautocomplete(selected.text || selected);
});
}
function searchandloadnote()
{
selectnote().then(selected =>
{
var title = selected.text || selected;
loadnote(title);
});
}
function currentistodo()
{
return istodo(title.value, md.value);
}
function istodo(title, content)
{
return title.includes("todo") || gettags(content).includes("todo");
}
function sorttodotxt(content)
{
var hat = headerandtext(content);
var olditems = hat.text.split("\n");
var prio = [];
var std = [];
var done = [];
olditems.forEach(item =>
{
if (item)
{
if (item.startsWith("("))
{
item = item.substring(4);
var priority = String.fromCharCode(65 + prio.length);
prio.push(`(${priority}) ${item}`);
}
else if (item.startsWith("x "))
{
done.push(item);
}
else
{
std.push(item);
}
}
});
prio = prio.sort((a,b) => a.localeCompare(b));
done = done.sort((a,b) => a.localeCompare(b));
var all = prio.concat(std).concat(done);
return hat.header + all.join("\n");
}
function sortcurrentastodo()
{
if (currentistodo())
{
var content = sorttodotxt(md.value);
seteditorcontent(content);
}
}
function searchandreplace()
{
var oldvalue = prompt("Search:");
if (!oldvalue)
{
return;
}
var newvalue = prompt("Replace by:");
if (!newvalue)
{
return;
}
var doit = confirm(`Replace '${oldvalue}' by '${newvalue}'?`);
if (!doit)
{
return;
}
seteditorcontent(md.value.replaceAll(oldvalue, newvalue));
}
function restoredeleted()
{
searchinlist(sortedlist(true).map(i => i.title))
.then(title =>
{
if (confirm("Restore " + title + "?"))
{
var guid = getguid(title);
metadata[guid].deleted = false;
metadata[guid].lastchanged = Date.now();
renameinternallinks(title + " (deleted)", title);
serializeindex();
}
});
}
function serializeindex()
{
serialize("index", JSON.stringify(metadata));
}
function deletenote(title)
{
var guid = getguid(title);
metadata[guid].deleted = true;
metadata[guid].lastchanged = Date.now();
renameinternallinks(title, title + " (deleted)");
serializeindex()
}
function deletecurrentnote()
{
if (confirm('delete "' + title.value + '"?'))
{
deletenote(title.value);
loadlast();
}
}
function restore()
{
if (confirm('restore "' + title.value + '"?'))
{
seteditorcontent(backup);
}
}
function insertheader()
{
if (preview.hidden && !md.value.startsWith("---\n"))
{
var headers = defaultheaders();
seteditorcontent(headers + md.value);
setpos(27);
}
resize();
}
function splitshortcut(s)
{
var r = {};
s = s.split("+");
r.key = s.pop();
s.forEach(e => {
r[e] = true;
})
return r;
}
function shortcutmatches(event, shortcut)
{
var s = splitshortcut(shortcut);
return (event.key == s.key && !(s.ctrl && !event.ctrlKey && !event.altKey) && !(s.shift && !event.shiftKey) && !(s.alt && !event.altKey))
}
function executecommand(command)
{
if (command.action)
{
command.action();
}
}
function esc(event)
{
if (!searchdialog.hidden)
{
event.preventDefault();
searchdialog.hidden = true;
filter.placeholder = "Search...";
md.focus();
}
else if (settings.uselinkpopup && typeof linkdialog != "undefined")
{
removelinkdialog();
}
else if (title.value == "Help" || title.value == "Search result")
{
loadlast();
}
else if (networkpage.hidden == false)
{
networkpage.hidden = true;
toggleeditor(false);
}
else if (preview.hidden == false)
{
togglepreview();
}
else
{
commandpalette();
}
}
function boldify()
{
var pos = currentrange();
var newcontent;
var offset = 2;
if (md.value.substr(pos.start - 2, 2) == "**" && md.value.substr(pos.end, 2) == "**")
{
newcontent = md.value.substr(0, pos.start - 2) + md.value.substr(pos.start, pos.end - pos.start) + md.value.substr(pos.end + 2);
offset = -2;
}
else
{
newcontent = md.value.substr(0, pos.start) + "**" + md.value.substr(pos.start, pos.end - pos.start) + "**" + md.value.substr(pos.end);
}
seteditorcontent(newcontent);
md.setSelectionRange(pos.start + offset, pos.end + offset);
}
function foreachmetadata(callback)
{
Object.keys(metadata).forEach(guid =>
{
callback(guid, metadata[guid]);
});
}
function snippetautocomplete()
{
var tocursor = md.value.substr(0, md.selectionStart)
var slashindex = tocursor.lastIndexOf("/");
if (slashindex > tocursor.lastIndexOf(" ") && slashindex > tocursor.lastIndexOf("\n"))
{
var commandbegin = tocursor.substr(slashindex);
var snippet = snippets.find(s => s.command.startsWith(commandbegin));
if (snippet)
{
insert(snippet.insert, snippet.cursor, commandbegin.length);
return true;
}
}
return false;
}
function mainkeydownhandler()
{
if (event.key == "Escape")
{
esc(event);
}
else if (!searchdialog.hidden && (event.key == "Tab" || event.keyCode == "40" || event.keyCode == "38"))
{
event.preventDefault();
fileindex += (event.shiftKey || event.keyCode == "38") ? -1 : 1;
fileindex = Math.min(fileindex, filteredlist.children.length - 1);
fileindex = Math.max(fileindex, 0);
applyfileindex();
}
else if (event.ctrlKey && event.key == " " || event.key == "F1")
{
commandpalette();
event.preventDefault();
}
else if (event.ctrlKey && event.key == "b" && md.selectionStart < md.selectionEnd)
{
boldify();
event.preventDefault();
}
else if (event.ctrlKey && event.shiftKey && (event.keyCode == "40" || event.keyCode == "38"))
{
var pos = currentrange();
var direction = event.keyCode == "40" ? 1 : -1;
var start = lineatpos(md.selectionStart);
var end = lineatpos(md.selectionEnd);
var lines = md.value.split("\n");
if (direction > 0 && end < lines.length || direction < 0 && start > 0)
{
var block = lines.splice(start, end - start + 1);
lines.splice(start + direction, 0, ...block);
seteditorcontent(lines.join("\n"));
var posshift = direction > 0 ? lines[start].length + 1 : - 1 - lines[end].length;
md.setSelectionRange(pos.start + posshift, pos.end + posshift);
}
event.preventDefault();
}
else if (event.ctrlKey || event.altKey)
{
// notes shortcuts
var found = false;
foreachmetadata((guid, item) =>
{
if (item.header.shortcut && shortcutmatches(event, item.header.shortcut))
{
console.log("Loading note '" + item.title + "' from header shortcut " + item.header.shortcut);
event.preventDefault();
loadnote(item.title);
found = true;
}
});
// commands shortcuts
if (!found)
{
commands.filter(c => c.shortcut)
.every(command =>
{
if (shortcutmatches(event, command.shortcut))
{
event.preventDefault();
executecommand(command);
return false;
}
return true;
});
}
}
}
function setwindowtitle()
{
document.title = title.value;
}
function renameinternallinks(from, to)
{
foreachmetadata( (guid, item) =>
{
var content = localStorage.getItem(guid);
var newcontent = content.replaceAll("[[" + from + "]]", "[[" + to + "]]");
if (content != newcontent)
{
serialize(guid, newcontent);
if (item.title == title.value)
{
seteditorcontent(newcontent);
}
}
});
}
function ontitlechange()
{
var guid = getguid(title.value);
if (guid)
{
showtemporaryinfo(title.value + " alreday exists");
title.value = previoustitle;
}
else
{
renameinternallinks(previoustitle, title.value);
guid = getguid(previoustitle);
previoustitle = title.value;
metadata[guid].title = title.value;
setwindowtitle();
flush();
if (!settings.titlebydefault)
{
toggletitle();
}
}
}
function simplifystring(str)
{
return str.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, "");
}
function applyfilter()
{
[...filteredlist.children].forEach(div =>
{
div.hidden = simplifystring(div.textContent).indexOf(simplifystring(filter.value)) < 0;
});
fileindex = 0;
applyfileindex();
}
function backspace(nb)
{
var pos = getpos();
var c = md.value;
seteditorcontent(c.substring(0, pos - nb) + c.substring(pos));
setpos(pos - nb);
}
function editorkeydown()
{
if (event.key == "Enter")
{
var line = md.value.substring(0, getpos()).split("\n").pop();
markerslist.filter(marker => line.startsWith(marker))
.every(marker =>
{
event.preventDefault();
if (line != marker)
{
insert("\n" + marker.replace("[x]", "[ ]"));
}
else
{
backspace(marker.length);
}
return false;
});
}
else if (event.key === "Tab")
{
event.preventDefault();
if (!snippetautocomplete())
{
var init = currentrange();
var range = getlinesrange();
range.start--;
range.end--;
var selection = md.value.substring(range.start, range.end);
var newtext;
if (event.shiftKey)
{
newtext = selection.replaceAll("\n ", "\n");
}
else
{
newtext = selection.replaceAll("\n", "\n ");
}
seteditorcontent(md.value.substring(0, range.start)
+ newtext
+ md.value.substring(range.end));
var shift = 0;
if (newtext.length < selection.length)
{
shift = -4;
}
else if (newtext.length > selection.length)
{
shift = 4;
}
md.selectionStart = init.start + shift;
md.selectionEnd = init.end + (newtext.length - selection.length);
}
}
else if (event.key === "[" && before(1) == "[")
{
event.preventDefault();
insert("[");
searchautocomplete();
}
else if (settings.tagautocomplete && event.key == " " && before(1) == "," && md.value.substring(0, getpos()).split("\n").pop().startsWith("tags: "))
{
event.preventDefault();
// search in tags list
console.log(event.key);
tagslist()
.then(tag =>
{
insert(" " + tag);
md.focus();
})
}
else
{
var snippet = snippets.find(s => before(s.command.length - 1) + event.key == s.command);
if (snippet)
{
event.preventDefault();
insert(snippet.insert, snippet.cursor, snippet.command.length - 1);
}
}
}
function insertautocomplete(selectednote)
{
md.focus();
insert(selectednote + "]] ");
}
function togglepreview()
{
preview.innerHTML = md2html(md.value);
toggleeditor(!md.hidden);
preview.hidden = !preview.hidden;
if (preview.hidden)
{
resize();
md.focus();
}
}
function withsubs()
{
// todo: handle loops
var currentguid = getguid(title.value);
var content = md.value;
var kids = children(currentguid);
while (kids.length)
{
kids.forEach(guid =>
{
var kidcontent = localStorage.getItem(guid);
if (kidcontent.startsWith("---\n"))
{
var pos = kidcontent.indexOf("---\n", 4);
kidcontent = kidcontent.substring(pos + 4);
}
content = content.replaceAll("[[" + metadata[guid].title + "]]", kidcontent);
});
kids = children(currentguid);
}
return content;
}
function togglepreviewwithsubs()
{
var note = withsubs();
if (note)
{
preview.innerHTML = md2html(note.content);
toggleeditor(!md.hidden);
preview.hidden = !preview.hidden;
if (preview.hidden)
{
resize();
md.focus();
}
}
}
function bind(title, content, pos)
{
if (workerid)
{
showtemporaryinfo("Cannot open '" + title + "' because current note not yet serialized");
return;
}
previoustitle = title;
backup = content;
window.title.value = title;
setwindowtitle();
md.value = content || "";
applycolors();
md.style.height = "0px";
resize();
setpos(pos || 0);
if (!issplit() && searchdialog.hidden)
{
// force to scroll to cursor pos
md.blur();
md.focus();
}
}
function defaultheaders(tags = "")
{
return [
"---",
"date: " + timestamp().substr(0,10),
"tags: " + (tags || ""),
"---",
"",""].join("\n");
}
function loadnote(title)
{
var guid = getguid(title);
if (!guid)
{
guid = createnote(title);
}
var content = localStorage.getItem(guid);
var item = metadata[guid];
delete item.deleted;
if (item.header.tags.includes("journal"))
{
// remove empty entries
content = content.replace(/\d{4}-\d{2}-\d{2}\n*(\d{4}-\d{2}-\d{2})/g, "$1");
// create new entry for today
var hat = headerandtext(content);
var today = timestamp().substr(0,10);
if (!hat.text.startsWith(today) && !hat.text.startsWith("\n" + today))
{
content = hat.header + "\n" + today + "\n\n" + hat.text;
item.pos = hat.header.length + 12;
}
}
if (settings.autosorttodo && istodo(title, content))
{
content = sorttodotxt(content);
}
bind(item.title, content, item.pos);
if (!preview.hidden || (preview.hidden && (gettags(md.value).indexOf("preview") !== -1)))
{
togglepreview();
}
}
function focuseditor()
{
if (document.documentElement == event.srcElement)
{
md.focus();
console.log("Forced focus");
}
}