Skip to content

Instantly share code, notes, and snippets.

@shyamzzp
Last active April 27, 2026 11:41
Show Gist options
  • Select an option

  • Save shyamzzp/7a6a352e948ea6c74e22d22c1568ebeb to your computer and use it in GitHub Desktop.

Select an option

Save shyamzzp/7a6a352e948ea6c74e22d22c1568ebeb to your computer and use it in GitHub Desktop.
Tampermonkey userscript: show Workflowy notes up to two lines
// ==UserScript==
// @name Workflowy Two-Line Notes
// @namespace https://github.com/shyamzzp/workflowy-cli
// @version 0.3.1
// @description Keep Workflowy notes visible for up to two lines, then truncate with an ellipsis.
// @match https://workflowy.com
// @match https://workflowy.com/*
// @match https://*.workflowy.com/*
// @include https://workflowy.com/#/*
// @include https://*.workflowy.com/#/*
// @run-at document-idle
// @grant none
// ==/UserScript==
(function () {
'use strict';
const STYLE_ID = 'workflowy-two-line-notes-style';
const CLASS_NAME = 'wf-two-line-note';
const PROJECT_CLASS_NAME = 'wf-two-line-note-project';
// Workflowy changes class names over time, so this intentionally uses a few
// likely note selectors plus a text/role fallback applied by the observer.
const noteSelectors = [
'.content .note',
'.content .notes',
'.project .note',
'.project .notes',
'.project .content .note',
'.project .content .notes',
'.nameAndNote .note',
'.nameAndNote .notes',
'.nameAndNotes .note',
'.nameAndNotes .notes',
'.notes',
'[data-testid*="note" i]',
'[class*="note" i]',
];
const projectSelectors = [
'.project',
'[class*="project" i]',
];
function installStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
.${CLASS_NAME} {
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: 2 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: normal !important;
line-height: 1.35 !important;
max-height: calc(1.35em * 2) !important;
min-height: min(calc(1.35em * 2), 100%) !important;
height: auto !important;
}
.${CLASS_NAME}:focus,
.${CLASS_NAME}:focus-within,
.${CLASS_NAME}[contenteditable="true"]:focus {
-webkit-line-clamp: unset !important;
max-height: none !important;
overflow: visible !important;
}
.${PROJECT_CLASS_NAME} .note,
.${PROJECT_CLASS_NAME} .notes,
.${PROJECT_CLASS_NAME} [class*="note" i],
.${PROJECT_CLASS_NAME} [data-testid*="note" i] {
display: -webkit-box !important;
-webkit-box-orient: vertical !important;
-webkit-line-clamp: 2 !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: normal !important;
line-height: 1.35 !important;
max-height: calc(1.35em * 2) !important;
height: auto !important;
min-height: 0 !important;
}
.${PROJECT_CLASS_NAME} .note:focus,
.${PROJECT_CLASS_NAME} .notes:focus,
.${PROJECT_CLASS_NAME} [class*="note" i]:focus,
.${PROJECT_CLASS_NAME} [data-testid*="note" i]:focus {
-webkit-line-clamp: unset !important;
max-height: none !important;
overflow: visible !important;
}
`;
document.head.appendChild(style);
}
function looksLikeEditableNote(element) {
if (!(element instanceof HTMLElement)) return false;
const className = String(element.className || '').toLowerCase();
const testId = String(element.getAttribute('data-testid') || '').toLowerCase();
const aria = String(element.getAttribute('aria-label') || '').toLowerCase();
if (className.includes('note') || testId.includes('note') || aria.includes('note')) {
return true;
}
return false;
}
function applyClamp(root = document) {
for (const selector of projectSelectors) {
root.querySelectorAll?.(selector).forEach((element) => {
if (element instanceof HTMLElement) {
element.classList.add(PROJECT_CLASS_NAME);
}
});
}
for (const selector of noteSelectors) {
root.querySelectorAll?.(selector).forEach((element) => {
if (element instanceof HTMLElement) {
element.classList.add(CLASS_NAME);
element.style.height = 'auto';
element.style.maxHeight = 'calc(1.35em * 2)';
element.style.webkitLineClamp = '2';
}
});
}
root.querySelectorAll?.('[contenteditable="true"], textarea').forEach((element) => {
if (looksLikeEditableNote(element)) {
element.classList.add(CLASS_NAME);
}
});
}
installStyle();
applyClamp();
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement) {
if (looksLikeEditableNote(node)) node.classList.add(CLASS_NAME);
for (const selector of projectSelectors) {
if (node.matches?.(selector)) node.classList.add(PROJECT_CLASS_NAME);
}
applyClamp(node);
}
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment