use contenteditable for task text so ellipsis works in firefox
Hunter
Thursday, June 4th, 2026 at 7:21 PM
88 --highlight: var(--burl);
89 --accent: var(--flame);
90 --icon: '⚔️';
- & input[type="text"] {
91 + .task-text {
92 font-family: BasteleurMoonlight, Arial, sans-serif;
93 }
94 }
99 --highlight: var(--ash);
100 --accent: var(--cinder);
101 --icon: '🪵';
- & input[type="text"] {
102 + .task-text {
103 font-family: BasteleurMoonlight, Arial, sans-serif;
104 }
105 .parent-task {
123 .custom-checkbox:checked + .checkbox-label::before {
124 display: none;
125 }
- & input[type="text"] {
126 + .task-text {
127 text-shadow: 1px 1px 1px var(--cola);
128 }
- input[type="text"]::selection {
129 + .task-text::selection {
130 color: var(--text);
131 }
132 }
137 --highlight: var(--muscat);
138 --accent: var(--muscat);
139 --icon: '🍇';
- & input[type="text"] {
140 + .task-text {
141 font-family: BasteleurMoonlight;
142 }
- input[type="text"]::selection {
143 + .task-text::selection {
144 background-color: var(--background);
145 color: var(--text);
146 text-shadow: none;
148 .active {
149 border-radius: 30px 0px 0px 30px;
150 }
- .active input[type="text"] {
151 + .active .task-text {
152 color: var(--bloom);
153 text-shadow: 1px 1px 2px black;
154 }
200 --highlight: var(--bakelite);
201 --accent: var(--mothball);
202 --icon: '👺';
- & input[type="text"] {
203 + .task-text {
204 font-family: BasteleurMoonlight, Arial, sans-serif;
205 }
- .active input[type="text"] {
206 + .active .task-text {
207 color: var(--mothball);
208 text-shadow: 1px 1px 1px var(--bakelite);
209 }
210 .parent-task {
211 --text: var(--mothball)
212 }
- .parent-task input[type="text"] {
213 + .parent-task .task-text {
214 text-shadow: 1px 1px 1px var(--bakelite);
215 }
216 .checkbox-label {
238 .active .custom-checkbox:indeterminate + .checkbox-label::before {
239 background-color: var(--mothball);
240 }
- input[type="text"]::selection {
241 + .task-text::selection {
242 background-color: var(--lipstick);
243 color: var(--mothball);
244 }
287 list-style-type: none;
288 padding-left: 20px;
289 }
-input[type="text"] {
- border: none;
- background: transparent;
290 +.task-text {
291 font-size: 1.17em;
292 font-weight: bold;
- width: calc(100% - 30px);
- margin-left: 5px;
- padding: 5px;
293 + flex: 1;
294 + min-width: 0;
295 + margin-left: 10px;
296 + padding: 5px 5px 5px 0;
297 color: var(--text);
298 overflow: hidden;
299 text-overflow: ellipsis;
300 white-space: nowrap;
301 + cursor: text;
302 + line-height: normal;
303 + min-height: 1.17em;
304 }
-input[type="text"]:focus {
305 +.task-text:focus {
306 outline: none;
307 }
-input[type="text"]::selection {
308 +.active .task-text {
309 + text-overflow: clip;
310 +}
311 +.task-text::selection {
312 background-color: var(--accent);
313 color: var(--background);
314 }
317 align-items: center;
318 margin: 5px 0;
319 padding-left: 11px;
320 + padding-right: 11px;
321 padding-top: 4px;
322 padding-bottom: 4px;
323 }
4 if (!e.target.closest('.task-container')) {
5 var activeTask = document.querySelector('.task-container.active');
6 if (activeTask) {
- var taskInput = activeTask.querySelector('input[type="text"]');
7 + var taskInput = activeTask.querySelector('.task-text');
8 taskInput.focus();
9 } else {
10 selectFirstSubtask();
65 document.addEventListener('keydown', handleSave);
66 document.addEventListener('keydown', handleOpen);
67
- // Reset cursor on non-focused inputs
68 + // Reset cursor on non-focused task-text elements
69 state.appContainer.addEventListener('focusin', function(e) {
- if (e.target.tagName === 'INPUT' && e.target.type === 'text') {
- document.querySelectorAll('input[type="text"]').forEach(input => {
70 + if (e.target.classList && e.target.classList.contains('task-text')) {
71 + document.querySelectorAll('.task-text').forEach(input => {
72 if (input !== e.target) {
73 placeCursorAtBeginning(input);
74 }
80 setInitialTheme();
81 renderCurrentView();
82 selectFirstSubtask();
83 +
84 + // re-pin the active caret's scroll once layout settles and once fonts load
85 + requestAnimationFrame(rescrollActiveCaret);
86 + if (document.fonts && document.fonts.ready) {
87 + document.fonts.ready.then(rescrollActiveCaret);
88 + }
89 });
1 // Render: view rendering, breadcrumbs, UI updates, and shared DOM helpers
2
3 +function getCaretOffset(el) {
4 + var sel = window.getSelection();
5 + if (!sel || sel.rangeCount === 0 || !el.contains(sel.anchorNode)) return null;
6 + var range = sel.getRangeAt(0);
7 + var pre = range.cloneRange();
8 + pre.selectNodeContents(el);
9 + pre.setEnd(range.endContainer, range.endOffset);
10 + return pre.toString().length;
11 +}
12 +
13 +function setCaretOffset(el, offset) {
14 + el.focus();
15 + var node = el.firstChild;
16 + var range = document.createRange();
17 + var sel = window.getSelection();
18 + var pos = 0;
19 + if (node && node.nodeType === Node.TEXT_NODE) {
20 + pos = Math.max(0, Math.min(offset, node.textContent.length));
21 + range.setStart(node, pos);
22 + } else {
23 + range.setStart(el, 0);
24 + }
25 + range.collapse(true);
26 + sel.removeAllRanges();
27 + sel.addRange(range);
28 + // browsers don't auto-scroll a programmatic caret, so do it manually
29 + scrollCaretIntoView(el, range, pos);
30 +}
31 +
32 +function scrollCaretIntoView(el, range, pos) {
33 + if (pos >= el.textContent.length) {
34 + el.scrollLeft = el.scrollWidth;
35 + return;
36 + }
37 + if (pos <= 0) {
38 + el.scrollLeft = 0;
39 + return;
40 + }
41 + var caretRect = range.getBoundingClientRect();
42 + var elRect = el.getBoundingClientRect();
43 + var pad = parseFloat(getComputedStyle(el).paddingRight) || 0;
44 + if (caretRect.right > elRect.right - pad) {
45 + el.scrollLeft += caretRect.right - (elRect.right - pad);
46 + } else if (caretRect.left < elRect.left) {
47 + el.scrollLeft -= elRect.left - caretRect.left;
48 + }
49 +}
50 +
51 +function selectAllText(el) {
52 + var range = document.createRange();
53 + range.selectNodeContents(el);
54 + var sel = window.getSelection();
55 + sel.removeAllRanges();
56 + sel.addRange(range);
57 +}
58 +
59 +function rescrollActiveCaret() {
60 + var el = document.querySelector('.task-container.active .task-text');
61 + if (el) setCaretOffset(el, getCaretOffset(el) || 0);
62 +}
63 +
64 +function isSelectionCollapsed() {
65 + var sel = window.getSelection();
66 + return !sel || sel.isCollapsed;
67 +}
68 +
69 function generateBreadcrumbs(rootTask, currentPath, selectedTaskId) {
70 var breadcrumbs = '';
71 var currentTask = rootTask;
118 }
119
120 function selectAndFocusTask(task, cursorPos) {
- var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
121 + var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] .task-text`);
122 if (taskInput) {
123 taskInput.focus();
- var pos = cursorPos != null ? cursorPos : taskInput.value.length;
- taskInput.setSelectionRange(pos, pos);
124 + var pos = cursorPos != null ? cursorPos : taskInput.textContent.length;
125 + setCaretOffset(taskInput, pos);
126 setActiveTask(taskInput, task);
127 }
128 }
129
130 function placeCursorAtBeginning(input) {
- input.setSelectionRange(0, 0);
131 + input.scrollLeft = 0;
132 }
133
134 function setActiveTask(input, task) {
187
188 function handleCopyAndCut(e) {
189 if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'x')) {
- var activeTaskInput = document.querySelector('.task-container.active input[type="text"]');
190 + var activeTaskInput = document.querySelector('.task-container.active .task-text');
191 if (activeTaskInput) {
192 e.preventDefault();
193
- if (activeTaskInput.selectionStart === activeTaskInput.selectionEnd) {
- activeTaskInput.select();
194 + // With no selection, act on the whole task text
195 + var collapsed = isSelectionCollapsed();
196 + if (collapsed) {
197 + selectAllText(activeTaskInput);
198 }
199
200 if (e.key === 'c') {
206 var taskId = taskContainer.dataset.id;
207 var task = state.currentTask.id === taskId ? state.currentTask : state.currentTask.subtasks.find(t => t.id === taskId);
208 if (task) {
- task.text = activeTaskInput.value;
209 + task.text = activeTaskInput.textContent;
210 scheduleSave();
211 }
212 }
213
- if (e.key === 'c' && activeTaskInput.selectionStart === 0 && activeTaskInput.selectionEnd === activeTaskInput.value.length) {
- activeTaskInput.setSelectionRange(activeTaskInput.value.length, activeTaskInput.value.length);
214 + // After a copy that auto-selected everything, collapse the caret to the end
215 + if (e.key === 'c' && collapsed) {
216 + setCaretOffset(activeTaskInput, activeTaskInput.textContent.length);
217 }
218 }
219 }
20 checkboxLabel.className = 'checkbox-label';
21 checkboxLabel.setAttribute('for', `checkbox-${task.id}`);
22
- var taskInput = document.createElement('input');
- taskInput.type = 'text';
- taskInput.value = task.text;
- taskInput.setAttribute('autocomplete', 'off');
23 + var taskInput = document.createElement('div');
24 + taskInput.className = 'task-text';
25 + taskInput.contentEditable = 'plaintext-only';
26 + taskInput.textContent = task.text;
27 taskInput.setAttribute('spellcheck', 'false');
28 taskInput.setAttribute('autocorrect', 'off');
29 taskInput.setAttribute('autocapitalize', 'off');
76 for (var t of selected) {
77 if (t.text.length > 0) {
78 t.text = t.text.slice(0, -1);
- var inp = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
- if (inp) inp.value = t.text;
79 + var inp = document.querySelector(`.task-container[data-id="${t.id}"] .task-text`);
80 + if (inp) inp.textContent = t.text;
81 }
82 }
83 keyHandler.backspace.canDelete = false;
84 scheduleSave();
85 return;
86 }
- if (taskInput.value === '' && keyHandler.backspace.canDelete && state.multiSelectedIds.length <= 1) {
87 + if (taskInput.textContent === '' && keyHandler.backspace.canDelete && state.multiSelectedIds.length <= 1) {
88 e.preventDefault();
89 if (task !== state.taskPath[0]) {
90 keyHandler.backspace.blocked = true;
97 } else {
98 applyShakeAnimation(task.id);
99 }
- } else if (taskInput.value !== '') {
100 + } else if (taskInput.textContent !== '') {
101 keyHandler.backspace.canDelete = false;
102 }
103 } else if (e.key === 'Enter' && !e.shiftKey) {
204 taskInput.addEventListener('keydown', handleCopyAndCut);
205 taskInput.addEventListener('input', (e) => {
206 var oldText = task.text;
- task.text = taskInput.value;
207 + // Keep text single-line: strip any newlines a paste/IME may introduce
208 + if (taskInput.textContent.indexOf('\n') !== -1) {
209 + var caret = getCaretOffset(taskInput);
210 + taskInput.textContent = taskInput.textContent.replace(/\n/g, '');
211 + if (caret != null) setCaretOffset(taskInput, caret);
212 + }
213 + task.text = taskInput.textContent;
214 // Propagate edits to all other multi-selected tasks
215 if (state.multiSelectedIds.length > 1 && state.multiSelectedIds.includes(task.id)) {
216 var otherSelected = getMultiSelectedTasks().filter(t => t.id !== task.id);
218 if (e.inputType === 'insertText' && e.data) {
219 for (var t of otherSelected) {
220 t.text = t.text + e.data;
- var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
- if (otherInput) otherInput.value = t.text;
221 + var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] .task-text`);
222 + if (otherInput) otherInput.textContent = t.text;
223 }
224 } else if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') {
225 for (var t of otherSelected) {
226 if (t.text.length > 0) {
227 t.text = t.text.slice(0, -1);
228 }
- var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
- if (otherInput) otherInput.value = t.text;
229 + var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] .task-text`);
230 + if (otherInput) otherInput.textContent = t.text;
231 }
232 } else if (e.inputType === 'insertFromPaste' || e.inputType === 'insertFromDrop') {
- var addedLen = taskInput.value.length - oldText.length;
233 + var addedLen = taskInput.textContent.length - oldText.length;
234 if (addedLen > 0) {
- var pastedText = taskInput.value.slice(taskInput.selectionStart - addedLen, taskInput.selectionStart);
235 + var caretPos = getCaretOffset(taskInput);
236 + var pastedText = taskInput.textContent.slice(caretPos - addedLen, caretPos);
237 for (var t of otherSelected) {
238 t.text = t.text + pastedText;
- var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`);
- if (otherInput) otherInput.value = t.text;
239 + var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] .task-text`);
240 + if (otherInput) otherInput.textContent = t.text;
241 }
242 }
243 }
7
8 function toggleTaskState(task) {
9 // Save cursor position before re-rendering
- var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
- var cursorPos = taskInput ? taskInput.selectionStart : null;
10 + var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] .task-text`);
11 + var cursorPos = taskInput ? getCaretOffset(taskInput) : null;
12
13 if (task.state === 1) {
14 if (task.subtasks.length === 0 || !task.subtasks.every(t => t.state === 1)) {
24 var backgroundColor = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
25 document.getElementById('themeColor').setAttribute('content', backgroundColor);
26 updateFavicon();
27 + rescrollActiveCaret();
28 + if (document.fonts && document.fonts.ready) {
29 + document.fonts.ready.then(rescrollActiveCaret);
30 + }
31 }
32
33 function setInitialTheme() {