Skip to content

Instantly share code, notes, and snippets.

@chroju
Last active March 13, 2024 00:22
Show Gist options
  • Save chroju/d19ef7178c061d20d2b1c4acccadabb6 to your computer and use it in GitHub Desktop.
Save chroju/d19ef7178c061d20d2b1c4acccadabb6 to your computer and use it in GitHub Desktop.
UserScripts
// ==UserScript==
// @name Workflowy Journal
// @namespace https://github.com/chroju
// @version 0.0.3
// @description Quickly create/select a daily journal item
// @author chroju
// @match https://workflowy.com/*
// @match https://beta.workflowy.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// Utility functions
// Find and return the first item with the given tag
//
// Example: const item = findTaggedItem(WF.rootItem(), '#mytag');
function findTaggedItem(item, tag) {
const myItemTags = WF.getItemTags(item).concat(
WF.getItemNoteTags(item));
const myTags = myItemTags.map(t => t.tag);
if (myTags.includes(tag)) {
// We found a matching item
return item
}
const tagCounts = item.isMainDocumentRoot() ?
getRootDescendantTagCounts() :
item.getTagManager().descendantTagCounts;
const tagList = tagCounts ? tagCounts.getTagList().map(t => t.tag) : [];
if (!tagList.includes(tag)) {
// We don't have a matching tag underneath us
return null
}
for (const child of item.getChildren()) {
const match = findTaggedItem(child, tag);
if (match) {
return match
}
}
// No match
return null
}
// Find and returns all items tagged with a given
//
// Example: const items = findTaggedItems(WF.rootItem(), '#mytag');
function findTaggedItems(item, tag) {
let found = [];
// Check the current item first
const myTags = WF.getItemTags(item).concat(
WF.getItemNoteTags(item)).map(t => t.tag);
if (myTags.includes(tag)) {
// We found a matching item
found.push(item);
}
// Look at tag counts to quickly decide whether to descend into children
const tagCounts = item.isMainDocumentRoot() ?
getRootDescendantTagCounts() :
item.getTagManager().descendantTagCounts;
const tagList = tagCounts ? tagCounts.getTagList().map(t => t.tag) : [];
if (tagList.includes(tag)) {
// The tag counds say that we have some matching items, so go through
// the chilren
for (const child of item.getChildren()) {
found.push(...findTaggedItems(child, tag));
}
}
return found
}
// Find a child item matching the given name, or create it if one isn't found.
//
// Example: const newItem = findOrCreate(myItem, 'Some Title')
function findOrCreateItem(parent, name) {
for (const candidateItem of parent.getChildren()) {
if (candidateItem.getName().indexOf(name.trim()) != -1) {
return candidateItem;
}
}
const newItem = WF.createItem(parent);
WF.setItemName(newItem, name);
return newItem;
}
// Wait until the document has finished loading and the workflowy header
// is available
//
// Example: onHeaderAvailable((header) => { ... });
function onHeaderAvailable(callback) {
setTimeout(() => {
const header = document.querySelector('.header');
if (header) {
callback(header);
} else {
// Try again until header is available
onHeaderAvailable(callback)
}
}, 500);
}
// Add a button to the left of the gear menu
//
// Example: addButton('X', doTheThing);
function addButton(icon, buttonCallback) {
onHeaderAvailable((header) => {
const button = document.createElement('div');
button.innerHTML = icon
// Style/Hover behavior like existing menu buttons
button.className = 'extension_button';
const buttonCss = `
.extension_button:hover {
background-color: rgb(66, 72, 75);
}
.extension_button {
font-weight: bold;
border-radius: 18px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
`;
// Add the style element, if it's not already there
if (!document.querySelector("#extension_button_style")) {
const style = document.createElement('style');
style.id = 'extension_button_style';
style.appendChild(document.createTextNode(buttonCss));
header.insertBefore(style, header.querySelector('.gearMenu'));
}
button.addEventListener('click', buttonCallback);
header.insertBefore(button, header.querySelector('.gearMenu'));
});
}
function getToday() {
const todayDate = new Date();
const offset = todayDate.getTimezoneOffset();
return new Date(todayDate.getTime() - (offset * 60 * 1000));
}
// Get today's date in ddd, MMM DD, YYYY format
//
// Example: const todayStr = getTodayString(); // Sun, Jan 21, 2024
function getTodayString(date) {
const dateStrings = date.toUTCString().split(' ');
return dateStrings[0] + ' ' + dateStrings[2] + ' ' + date.getUTCDate() + ', ' + dateStrings[3] + ' ';
}
function getMonthString(date) {
const monthStrings = date.toUTCString().split(' ');
return monthStrings[2] + ' ' + monthStrings[3];
}
function getTimeString(date) {
return ('0' + date.getUTCHours()).slice(-2) + ':' + ('0' + date.getUTCMinutes()).slice(-2);
}
function getWeekNum(date){
const today = new Date();
const year = today.getFullYear()
const firstDate = new Date(`${year}-01-01T00:00:00.000Z`);
const firstDateDay = firstDate.getDay() == 0 ? 7 : firstDate.getDay();
const firstWeekLastDate = 8 - firstDateDay;
const NthDayToday = Math.floor((today.getTime() - firstDate.getTime())/(24 * 60 * 60 * 1000) + 1);
const weekNum = Math.ceil((NthDayToday - firstWeekLastDate)/7) +1;
return `${year} W${('0' + weekNum).slice(-2)}`;
}
// Set multiple styles at once
function setStyle(elem, styles) {
for (let style in styles) {
elem.style[style] = styles[style]
}
}
// Add a style element (to add actual CSS from JS)
function addStylesheet(stylesheet) {
const element = document.createElement('style')
element.innerHTML = stylesheet;
document.head.insertBefore(element, null);
}
// Main script
// The tag to look for as the root of the journal
const journalItemTag = '#journalroot';
function createJournalItem() {
const journalItem = findTaggedItem(WF.rootItem(), journalItemTag);
if (!journalItem) {
WF.showMessage(`Unable to find journal root item`, true);
return;
}
const today = getToday();
const todayStr = getTodayString(today);
// const monthStr = getMonthString(today);
const wnStr = getWeekNum(today);
// Create child item
WF.editGroup(function () {
// Find the month item
const todayItem = findOrCreateItem(journalItem, todayStr);
// Select the item and put the cursor in the right place
WF.zoomTo(todayItem);
WF.editItemName(todayItem);
});
}
function createInterstitialItem() {
const journalItem = findTaggedItem(WF.rootItem(), journalItemTag);
if (!journalItem) {
WF.showMessage(`Unable to find journal root item`, true);
return;
}
const today = getToday();
const todayStr = getTodayString(today);
// const monthStr = getMonthString(today);
const timeStr = getTimeString(today);
// Create child item
WF.editGroup(function () {
// Find the month item
const todayItem = findOrCreateItem(journalItem, todayStr);
const timeItem = findOrCreateItem(todayItem, timeStr);
const target = findOrCreateItem(timeItem, '');
// Select the item and put the cursor in the right place
WF.zoomTo(todayItem);
WF.moveItems([timeItem], todayItem, 999);
WF.expandItem(timeItem);
WF.zoomIn(timeItem);
});
}
// Pencil symbol
addButton('✎', createJournalItem);
// Add keyboard shortcut
document.addEventListener("keydown", function (event) {
if (!event.altKey && event.shiftKey &&
event.ctrlKey && !event.metaKey && event.key === "J") {
createJournalItem();
event.preventDefault();
}
});
document.addEventListener("keydown", function (event) {
if (!event.altKey && event.shiftKey &&
event.ctrlKey && !event.metaKey && event.key === "I") {
createInterstitialItem();
event.preventDefault();
}
});
document.addEventListener("keydown", function (event) {
if (!event.altKey && !event.shiftKey &&
event.ctrlKey && event.metaKey && event.keyCode === 48) {
var s = WF.focusedItem();
var t = s.getParent();
WF.editItemName(t);
WF.collapseItem(t)
if(t.getParent().getParent()==null){
WF.zoomOut();
}
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment