Jotter
Nearly 3 years ago, I made a little Progressive Web App (PWA) with a simple goal. I wanted a web-based version of Tyke. It’s the simplest Mac app ever: a text box that pops out of your menu bar so you can take quick notes.
I didn’t like that Tyke was native only and a rather small window too, so I created Jotter back in October 2019.
All it is, is a <textarea>
that if JavaScript is available (not when, if), a little app kicks in and syncs the content of that <textarea>
with local storage.
For those interested, this is the app.js
in all its glory:
import Content from './components/content.js';
class App {
constructor() {
this.today = new Date();
this.todayLabel = document.querySelector('[for="jotterDay"]');
this.weekLabel = document.querySelector('[for="jotterWeek"]');
this.contentInstance = new Content();
this.themeToggle = document.querySelector('[data-element="theme-toggle"]');
this.jotterInstances = document.querySelectorAll('[data-element="jotter"]');
// Show the button now that the JS is ready
this.themeToggle.removeAttribute('hidden');
// Set todays date on day label
this.todayLabel.innerText = `Day notes (${this.today.toLocaleDateString(
navigator.languages ? navigator.languages[0] : 'en-GB',
{
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}
)})`;
// Set week number on week notes
// Logic nicked from here: https://gist.github.com/IamSilviu/5899269
if (this.weekLabel) {
const firstOfJan = new Date(this.today.getFullYear(), 0, 1);
this.weekLabel.innerText = `Week notes (Week ${
Math.ceil(((this.today - firstOfJan) / 86400000 + firstOfJan.getDay() + 1) / 7) - 1
})`;
}
// Run the initial content application and the listener
this.applyContent();
this.listen();
// Set theme auto-apply and toggle apply
this.applyTheme();
this.themeToggle.addEventListener('click', () => {
this.applyTheme(true);
});
// Prevent save shortcut
document.addEventListener(
'keydown',
function (e) {
if ((window.navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey) && e.keyCode == 83) {
e.preventDefault();
}
},
false
);
}
// Listens for the jotter box content changes and applies to local storage accordingly
listen() {
this.jotterInstances.forEach((jotter) => {
jotter.addEventListener('input', () => {
this.contentInstance.save(jotter.getAttribute('data-id'), jotter.value);
});
});
// If content changes in another tab, sync it up
window.addEventListener('storage', (evt) => {
const {isTrusted} = evt;
if (!isTrusted) {
return;
}
this.applyContent();
});
// Toggle focus attribute on body when jotter is focused or blurred
this.jotterInstances.forEach((jotter) =>
jotter.addEventListener('focus', () => document.body.setAttribute('data-state', 'focus'))
);
this.jotterInstances.forEach((jotter) =>
jotter.addEventListener('blur', () => document.body.removeAttribute('data-state'))
);
}
// Loads the content from the conten interface and applies it
applyContent() {
this.jotterInstances.forEach(
(jotter) => (jotter.value = this.contentInstance.load(jotter.getAttribute('data-id')))
);
}
// Apply the dark mode or light mode and optionally store in localStorage
applyTheme(store = false) {
let currentSetting = localStorage.getItem('user-color-scheme');
// No storage found, so try to parse from CSS
if (!currentSetting) {
currentSetting = getComputedStyle(document.documentElement).getPropertyValue('--color-mode');
if (currentSetting.length) {
currentSetting = currentSetting.replace(/\"|'/g, '').trim();
localStorage.setItem('user-color-scheme', currentSetting);
}
}
// Store in localStorage if required
if (store) {
const reverseSetting = currentSetting === 'dark' ? 'light' : 'dark';
localStorage.setItem('user-color-scheme', reverseSetting);
// Apply the reversed setting to HTML element and toggle button
document.documentElement.setAttribute('data-user-color-scheme', reverseSetting);
this.themeToggle.innerText = reverseSetting === 'dark' ? 'Light mode' : 'Dark mode';
} else {
// Apply current setting setting to HTML element and toggle button
document.documentElement.setAttribute('data-user-color-scheme', currentSetting);
this.themeToggle.innerText = currentSetting === 'dark' ? 'Light mode' : 'Dark mode';
}
}
}
const appInstance = new App();
Good ol’ vanilla JavaScript with no build step. It runs like a dream and Just Works™.
I only added one “major feature” since then: the ability to have weekly and daily notes next to each other:
This is the key point of this post, really. Because I kept the tech stack super simple and built with progressive enhancement at the forefront, I barely ever have to touch it any more. I shipped a little update today, a simple view with no visible labels, which is the first time I’ve touched it since 2021!
It’s also a good case study of keeping things simple and then maintaining that attitude. Jotter doesn’t ever need to do anything more complex than it already does. Hell, it would be nice to have some sort of storage sync, but I think until browsers sync local storage with your other devices, it’ll just stay local because I really don’t want to add a complex build step or back-end.
The best thing about Jotter is that I know that whenever I need to make some notes, it will be there—ready—and it’ll work, because if all else fails, it’s just a humble little <textarea>
. I’d honestly say its the work I’m most proud of in my entire near 15 year career as a designer and developer because of this.
So, if you need a quick web-based notepad that’ll always be there for you: give Jotter a try today!
👋 Hello, I’m Andy and I’ll help you build fast & visually stunning websites.
I’m the founder of Set Studio, a creative agency that specialises in building stunning websites that work for everyone. If you’ve got a project in mind, get in touch.
Back to blog