-
-
Save thesved/79371d0c1dd34b6750c846368b323113 to your computer and use it in GitHub Desktop.
Copy this to roam/js page, including the "{{[[roam/js]]}}" node: | |
- {{[[roam/js]]}} | |
- ```javascript | |
/* | |
* Roam template PoC by @ViktorTabori | |
* 0.1alpha | |
* | |
* How to install it: | |
* - go to `roam/js` page` | |
* - make a new node: {{[[roam/js]]}} | |
* - put this code under that node | |
* - set type to javascript and allow the js to run | |
* - create a template page with some content: [[template]]/test | |
* - write :test: to you daily page and see what happens | |
* | |
* known issues: | |
* - looks hacky | |
* - for longer templates it messes up some lines | |
*/ | |
document.addEventListener('input', function(e){ | |
if ('_templateHook' in window) { | |
setTimeout(function(){ window._templateHook(e); }, 0); | |
} | |
}); | |
window._templateHook = async function(e) { | |
// logging | |
window._e = e; | |
// exit if not target | |
var elem = e.target | |
if (elem.nodeName != 'TEXTAREA' || e.data != ':') return; | |
console.log('ok',elem.value, elem); | |
// nativeValueSetter to bypass | |
var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,"value").set; | |
// resolve templates | |
var tab = 0; | |
var text = elem.value; | |
elem.value.replace(/:([^:]+):/g, async function(_, v, position){ | |
// lookup template | |
var tmp = getTemplate(v); | |
// if no result | |
if (!tmp) { | |
return _; | |
} | |
console.log('template:',v,tmp); | |
// remove first | |
tmp = tmp.replace(/^\s*- /,'').split("\n"); | |
// process first line | |
var line = tmp.shift(); | |
text = text.replace(_, line); | |
// handle heading | |
heading = (line.match(/^(#*) ?/)||['',''])[1].length; | |
if (heading > 0) { | |
console.log('heading:', heading, line.match(/^(#*) ?/)); | |
KeyboardLib.changeHeading(heading); | |
} | |
line = line.replace(/^#* ?/, ''); | |
if (line == '') line = ' '; | |
// set value | |
nativeSetter.call(e.target, line); | |
e.target.selectionStart = position; | |
e.target.selectionEnd = position; | |
e.target.dispatchEvent(new Event('input', {bubbles: true, cancelable: true })); | |
// process lines | |
while (tmp.length) { | |
// get new line | |
elem.focus(); | |
elem.selectionStart = elem.value.length; | |
elem.selectionEnd = elem.value.length; | |
// get new line and row | |
await KeyboardLib.pressEnter(); | |
elem = KeyboardLib.getActiveEditElement(); | |
line = tmp.shift(); | |
// handle tabs | |
console.log('line:', line) | |
tab = line.match(/^\s*/)[0].length/2-tab; // tab difference | |
console.log('tab:',tab); | |
if (tab > 0) { | |
for (var i=0; i<tab; i++) { | |
await KeyboardLib.pressTab() | |
} | |
} else if (tab < 0) { | |
for (var i=0; i<-tab; i++) { | |
await KeyboardLib.pressShiftTab(); | |
} | |
} | |
tab = line.match(/^\s*/)[0].length/2; // save current tab length | |
// handle heading | |
heading = (line.match(/^\s*- (#*) ?/)||['',''])[1].length; | |
if (heading > 0) { | |
console.log('heading:', heading, line.match(/^\s*- (#*) ?/)); | |
KeyboardLib.changeHeading(heading); | |
} | |
// set element value | |
elem = KeyboardLib.getActiveEditElement(); | |
line = line.replace(/^\s*- #* ?/,''); | |
if (line == '') line = ' '; | |
nativeSetter.call(elem, line); | |
elem.selectionStart = elem.value.length; | |
elem.selectionEnd = elem.value.length; | |
console.log('dispatch event'); | |
elem.dispatchEvent(new Event('input', {bubbles: true, cancelable: true })); | |
await KeyboardLib.delay(150); | |
} | |
}); | |
} | |
window.getTemplate = function(name) { | |
/* resolve node function by @ViktorTabori | |
* id: node id | |
* level: depth needed for indention | |
* trail: list of ids to avoid loops | |
* resolve: resolve block references and embeds starting with an exclamation mark: !{{embed:((blockid))}} and !((blockid)) | |
* skipFirstPrefix: no prefix, needed for block embeds and references | |
* stop: doesn't resolve children, needed for block reference resolution | |
*/ | |
function resolveNode(id, level, trail, resolve, skipFirstPrefix, stop){ | |
var level = level || 0; // for indentation | |
var trail = Object.assign({}, trail); // to avoid loops | |
var prefix = skipFirstPrefix ? '' : ' '.repeat(2*Math.max(level-1,0)) + '- '; // indention starting from level 2 | |
var newLine = skipFirstPrefix && stop ? '' : "\n"; // no new line when we resolve simple block references | |
var ret = ''; | |
// avoid loops: skip if trail already contains id | |
if (trail[id]) return; | |
trail[id] = true; | |
// get node info | |
var node = window.roamAlphaAPI.pull("[*]",id); | |
// node order | |
var order = node[':block/order'] || 0; | |
// add heading to prefix | |
if (node[':block/heading'] && node[':block/heading'] > 0) { | |
prefix += '#'.repeat(node[':block/heading'])+' '; | |
} | |
// current node string | |
if (typeof node[':block/string'] != 'undefined') { | |
// resolve block EMBEDs | |
var regexEmbed = resolve ? /!?{{\[*embed\]*\s*:\s*\(\(([^\)]*)\)\)\s*}}/ig : /!{{\[*embed\]*\s*:\s*\(\(([^\)]*)\)\)\s*}}/ig; | |
node[':block/string'] = node[':block/string'].replace(regexEmbed, function(_, v){ | |
var uid = v.trim(); | |
var id = window.roamAlphaAPI.q("[:find ?e :in $ ?a :where [?e :block/uid ?a]]", uid); | |
if (id.length == 0) { | |
return _; | |
} | |
var block = resolveNode(id[0][0], level, trail, true, true); // resolve node, no prefix | |
if (typeof block != 'undefined') { // for loops we got back undefined | |
return block; | |
} else { | |
return 'LOOP:'+_; | |
} | |
}); | |
// resolve block REFERENCEs | |
var regexReference = resolve ? /!?\(\(([^\)]*)\)\)/ig : /!\(\(([^\)]*)\)\)/ig; | |
node[':block/string'] = node[':block/string'].replace(regexReference, function(_, v){ | |
var uid = v.trim(); | |
var id = window.roamAlphaAPI.q("[:find ?e :in $ ?a :where [?e :block/uid ?a]]", uid); | |
if (id.length == 0) { | |
return _; | |
} | |
var block = resolveNode(id[0][0], level, trail, true, true, true); // resolve node, no prefix, don't resolve children | |
if (typeof block != 'undefined') { // for loops we got back undefined | |
return block; | |
} else { | |
return 'LOOP:'+_; | |
} | |
}); | |
// add block text to return | |
ret += prefix + node[':block/string'] + newLine; | |
} | |
// handle children | |
if (node[':block/children'] && !stop) { | |
var children = []; | |
var tmp; | |
// get children data | |
for (var i in node[':block/children']) { | |
tmp = resolveNode(node[':block/children'][i][':db/id'], level+1, trail); | |
if (typeof tmp != 'undefined') { | |
children.push(tmp); | |
} | |
} | |
// sort children in order | |
children.sort(function(a,b){return a.order-b.order}) | |
// concat children text | |
ret += children.map(function(i){return i.txt}).join(''); | |
} | |
// return based on how deep we are in the graph | |
if (level == 0 || skipFirstPrefix) { | |
return ret; | |
} else { | |
return {txt: ret, order: order}; | |
} | |
} | |
// check API endpoint | |
if (!window.roamAlphaAPI || !window.roamAlphaAPI.q || !window.roamAlphaAPI.pull) return; // no api endpoint | |
// search node ID | |
var nodeId; // page we look for | |
var search = ['template','[[template]]']; // search for template in template/name, [[template]]/name, ... | |
for (var i in search) { | |
nodeId = window.roamAlphaAPI.q("[:find ?e :in $ ?a :where [?e :node/title ?a]]", search[i]+'/'+name); | |
if (nodeId.length) { | |
nodeId = nodeId[0][0]; | |
break; | |
} | |
} | |
if (!nodeId || nodeId.length == 0) return; // no such template | |
return resolveNode(nodeId); | |
} | |
window.KeyboardLib = { | |
// thank you @VladyslavSitalo for the awesome Roam Toolkit, and the basis for this code | |
LEFT_ARROW: 37, | |
UP_ARROW: 38, | |
RIGHT_ARROW: 39, | |
DOWN_ARROW: 40, | |
BASE_DELAY: 20, | |
delay(millis) { | |
return new Promise(resolve => setTimeout(resolve, millis)) | |
}, | |
getKeyboardEvent: function(type, code, opts) { | |
return new KeyboardEvent(type, { | |
bubbles: true, | |
cancelable: true, | |
keyCode: code, | |
...opts, | |
}) | |
}, | |
getActiveEditElement: function() { | |
// stolen from Surfingkeys. Needs work. | |
var element = document.activeElement | |
// on some pages like chrome://history/, input is in shadowRoot of several other recursive shadowRoots. | |
while (element.shadowRoot) { | |
if (element.shadowRoot.activeElement) { | |
element = element.shadowRoot.activeElement | |
} else { | |
var subElement = element.shadowRoot.querySelector('input, textarea, select') | |
if (subElement) { | |
element = subElement | |
} | |
break | |
} | |
} | |
return element | |
}, | |
async simulateSequence(events, delayOverride) { | |
;events.forEach(function(e){ | |
return KeyboardLib.getActiveEditElement().dispatchEvent(KeyboardLib.getKeyboardEvent(e.name, e.code, e.opt)); | |
}); | |
return this.delay(delayOverride || this.BASE_DELAY); | |
}, | |
async simulateKey(code, delayOverride, opts) { | |
return this.simulateSequence([{name:'keydown', code:code, opt:opts}, {name:'keyup', code:code, opt:opts}], delayOverride); | |
}, | |
async changeHeading(heading, delayOverride) { | |
return this.simulateSequence( | |
[ | |
{name:'keydown', code:18, opt:{altKey:true}}, | |
{name:'keydown', code:91, opt:{metaKey:true}}, | |
{name:'keydown', code:48+heading, opt:{altKey:true, metaKey:true}}, | |
{name:'keyup', code:91, opt:{altKey:true}}, | |
{name:'keyup', code:18, opt:{}} | |
], | |
delayOverride); | |
}, | |
async pressEnter(delayOverride) { | |
return this.simulateKey(13, delayOverride) | |
}, | |
async pressEsc(delayOverride) { | |
return this.simulateKey(27, delayOverride) | |
}, | |
async pressBackspace(delayOverride) { | |
return this.simulateKey(8, delayOverride) | |
}, | |
async pressTab(delayOverride) { | |
return this.simulateKey(9, delayOverride) | |
}, | |
async pressShiftTab(delayOverride) { | |
return this.simulateKey(9, delayOverride, {shiftKey: true}) | |
}, | |
async pressCtrlV(delayOverride) { | |
return this.simulateKey(118, delayOverride, {metaKey: true}) | |
}, | |
getInputEvent() { | |
return new Event('input', { | |
bubbles: true, | |
cancelable: true, | |
}) | |
}, | |
}``` |
This is a great template, thank you! I've been using it for retros and for me, personally, it would be great to have relative dates. Right now, if I say "Review dailies from 2 weeks ago," the 2 weeks is static. Would you consider adding some code for relative dates?
Wow, much better than my userscript! I forked it and added the ability to include custom date/time variables (I added {{current_time}} and {{today}} ) with moment.js
https://gist.github.com/everruler12/56799ea648fea1bf3c53d023f1fee9ca
Thank you for this template! It helps me a lot in writing my daliy note!
And I find a small bug in handling heading part. when I add a Tag like '#dosomething' to the start of a line. The value of this line will be
'- #dosomething'. So the statement in line 113 (line = line.replace(/^\s*- #* ?/,'');
) will match '- #' in the value and replace it by ''.
I solve this bug by changing all the regex string which used to handle heading to /^\s*- #* +?/
. I use statements line = line.replace(/^\s*- /,'');line = line.replace(/(#+) +?/,'');
to replace the statement in line 113. Maybe this solution will give you some help.
Thank you for this template! It helps me a lot in writing my daliy note!
And I find a small bug in handling heading part. when I add a Tag like '#dosomething' to the start of a line. The value of this line will be
'- #dosomething'. So the statement in line 113 (line = line.replace(/^\s*- #* ?/,'');
) will match '- #' in the value and replace it by ''.I solve this bug by changing all the regex string which used to handle heading to
/^\s*- #* +?/
. I use statementsline = line.replace(/^\s*- /,'');line = line.replace(/(#+) +?/,'');
to replace the statement in line 113. Maybe this solution will give you some help.
I'm experiencing this bug not just if it's the first line but any use of #
as opposed to [[]]
seems to break the script. Does your fix fix that too?
this isn't working for me
It took me a while but it looks like I have the script up and running. However, the templates do not carry over nicely as of now. Even three flat lines make one line disappear. Not sure how to get onto this one but really like the idea!
I found that if I have a line with tags in it, I have to ensure there is a space after the last tag.
If the line looks like this labels:: #awesome
the line does not show up
But this does work:
labels:: #awesome
(there is a space after the e
in awesome)
found this page through this Youtube video. Nice demo of how this plugin works. - Works great for me. love it thanks.
Would it be possible to have it insert text instead of replace the entire line?
for instance:
As I type something :template:
This would produce
As I type something **text gets added here**
Thanks for hacking this together. You're my hero.