Skip to content

Commit 124e6f0

Browse files
mobile UX improvements: double tap priotize, long press edit, style changes (#39)
1 parent 932fd82 commit 124e6f0

File tree

3 files changed

+115
-41
lines changed

3 files changed

+115
-41
lines changed

manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "todotxt-codeblocks",
33
"name": "TodoTxt Codeblocks",
4-
"version": "0.7.3",
4+
"version": "0.8.0",
55
"minAppVersion": "0.15.0",
66
"description": "Manage your tasks inside codeblocks according to the Todo.txt specification.",
77
"author": "Benjamin Nguyen",

src/model/TodoItem.ts

+85-40
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { v4 as randomUUID } from 'uuid';
2-
import { ActionButton, ActionType, type ViewModel } from '.';
2+
import { ActionButton, ActionType, TodoList, type ViewModel } from '.';
33
import { Item } from './Item';
44
import { EditItemModal } from 'src/component';
55
import { moment } from 'obsidian';
66
import { processExtensions, ExtensionType } from 'src/extension';
7-
import { updateTodoItemFromEl } from 'src/stateEditor';
7+
import { update, updateTodoItemFromEl } from 'src/stateEditor';
88
import { ActionButtonV2 } from './ActionButtonV2';
99
import { DEFAULT_SETTINGS } from 'src/settings';
1010
import { SETTINGS_READ_ONLY } from 'src/main';
1111

12+
const DOUBLE_TAP_DELAY_MS = 300;
13+
const LONG_PRESS_THRESHOLD_MS = 600;
14+
1215
export default class TodoItem extends Item implements ViewModel {
1316
static HTML_CLS = 'todotxt-item';
1417
static ID_REGEX = /^item-\S+-(\d+)$/;
1518

1619
#id: string;
20+
private _lastTap: number;
21+
private _longPressTimer: NodeJS.Timeout;
1722

1823
constructor(text: string) {
1924
super(text);
@@ -54,7 +59,7 @@ export default class TodoItem extends Item implements ViewModel {
5459

5560
itemDiv.append(
5661
this.buildDescriptionHtml(),
57-
this.buildActionsHtml(),
62+
this.buildActionsHtml(itemDiv),
5863
);
5964

6065
return itemDiv;
@@ -201,39 +206,43 @@ export default class TodoItem extends Item implements ViewModel {
201206
}
202207

203208
// @ts-ignore
204-
if (!app.isMobile && !this.complete()) {
205-
// WYSIWYG editting
206-
description.setAttr('tabindex', 0);
207-
description.contentEditable = 'true';
208-
209-
description.addEventListener('focus', e => {
210-
description.spellcheck = true;
211-
});
212-
213-
description.addEventListener('blur', e => {
214-
description.spellcheck = false;
215-
if (description.textContent) {
216-
description.textContent = description.textContent.trimEnd();
217-
}
218-
if (description.textContent !== this.getBody()) {
219-
this.setBody(description.textContent ?? '');
220-
updateTodoItemFromEl(description, this);
221-
}
222-
});
223-
224-
description.addEventListener('keydown', e => {
225-
// console.log(e.key)
226-
if (e.key === 'Enter') {
227-
e.preventDefault();
228-
description.blur();
229-
} else if (e.key === 'Escape') {
230-
e.preventDefault();
231-
description.textContent = this.getBody();
232-
description.blur();
233-
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
234-
e.preventDefault();
235-
}
236-
});
209+
if (app.isMobile) {
210+
this.addMobileEventListeners(description);
211+
} else {
212+
if (!this.complete()) {
213+
// WYSIWYG editting
214+
description.setAttr('tabindex', 0);
215+
description.contentEditable = 'true';
216+
217+
description.addEventListener('focus', e => {
218+
description.spellcheck = true;
219+
});
220+
221+
description.addEventListener('blur', e => {
222+
description.spellcheck = false;
223+
if (description.textContent) {
224+
description.textContent = description.textContent.trimEnd();
225+
}
226+
if (description.textContent !== this.getBody()) {
227+
this.setBody(description.textContent ?? '');
228+
updateTodoItemFromEl(description, this);
229+
}
230+
});
231+
232+
description.addEventListener('keydown', e => {
233+
// console.log(e.key)
234+
if (e.key === 'Enter') {
235+
e.preventDefault();
236+
description.blur();
237+
} else if (e.key === 'Escape') {
238+
e.preventDefault();
239+
description.textContent = this.getBody();
240+
description.blur();
241+
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
242+
e.preventDefault();
243+
}
244+
});
245+
}
237246
}
238247

239248
return description;
@@ -295,11 +304,12 @@ export default class TodoItem extends Item implements ViewModel {
295304
}
296305
}
297306

298-
private buildActionsHtml(): HTMLSpanElement {
307+
private buildActionsHtml(el: HTMLElement): HTMLSpanElement {
299308
const actions = document.createElement('span');
300309
actions.className = 'todotxt-item-actions';
301310

302-
if (this.priority() === null && !this.complete()) {
311+
// @ts-ignore
312+
if (!app.isMobile && this.priority() === null && !this.complete()) {
303313
const prioritizeBtn = new ActionButtonV2(
304314
ActionType.STAR,
305315
e => this.prioritize(e),
@@ -308,7 +318,6 @@ export default class TodoItem extends Item implements ViewModel {
308318
}
309319

310320
actions.append(
311-
new ActionButton(ActionType.EDIT, EditItemModal.ID, this.id).render(),
312321
new ActionButton(ActionType.DEL, 'todotxt-delete-item', this.id).render(),
313322
);
314323

@@ -346,9 +355,45 @@ export default class TodoItem extends Item implements ViewModel {
346355
return select;
347356
}
348357

349-
private prioritize(e: MouseEvent) {
358+
private prioritize(e: Event) {
350359
const t = e.target as SVGElement;
351360
this.setPriority(SETTINGS_READ_ONLY.defaultPriority ?? DEFAULT_SETTINGS.defaultPriority);
352361
updateTodoItemFromEl(t, this);
353362
}
363+
364+
private addMobileEventListeners(el: HTMLElement) {
365+
const prioritizeOnDoubleTap = (e: Event) => {
366+
const currentTime = new Date().getTime();
367+
const tapLength = currentTime - this._lastTap;
368+
if (tapLength < DOUBLE_TAP_DELAY_MS && tapLength > 0) {
369+
e.preventDefault();
370+
if (!this.priority()) {
371+
this.prioritize(e);
372+
}
373+
}
374+
this._lastTap = currentTime;
375+
};
376+
el.addEventListener('touchend', prioritizeOnDoubleTap);
377+
378+
// edit on long press
379+
el.addEventListener('touchend', () => clearTimeout(this._longPressTimer));
380+
el.addEventListener('touchstart', () => {
381+
this._longPressTimer = setTimeout(() => {
382+
this.openEditModal(el);
383+
}, LONG_PRESS_THRESHOLD_MS);
384+
});
385+
};
386+
387+
private openEditModal(el: HTMLElement) {
388+
const editModal = new EditItemModal(this.asInputText(), el, (result) => {
389+
const { todoList, from, to } = TodoList.from(el);
390+
if (this.toString() === result.toString()) return;
391+
todoList.removeItem(this.idx!);
392+
todoList.add(result);
393+
update(from, to, todoList);
394+
});
395+
editModal.open();
396+
editModal.textComponent.inputEl.select();
397+
editModal.textComponent.inputEl.selectionStart = editModal.item.asInputText().length;
398+
}
354399
}

styles.css

+29
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,23 @@ If your plugin does not need CSS, delete this file.
4848
margin-block-end: var(--p-spacing);
4949
}
5050

51+
.is-phone .todotxt-language-line {
52+
margin-block-end: 0;
53+
}
54+
5155
h2.todotxt-list-title {
5256
margin-block-start: 0;
5357
margin-block-end: 0;
5458
}
5559

60+
.todotxt-list>.todotxt-item:nth-child(even) {
61+
background-color: var(--background-modifier-hover);
62+
}
63+
64+
.project-group-list>.todotxt-item:nth-child(even) {
65+
background-color: var(--background-modifier-hover);
66+
}
67+
5668
.todotxt-item {
5769
padding-top: var(--list-spacing);
5870
padding-bottom: var(--list-spacing);
@@ -61,6 +73,11 @@ h2.todotxt-list-title {
6173
padding-inline-start: 28px;
6274
}
6375

76+
.is-phone .todotxt-item {
77+
padding-top: 0.8rem;
78+
padding-bottom: 0.8rem;
79+
}
80+
6481
.todotxt-item .todotxt-priority {
6582
top: -0.1em;
6683
position: relative;
@@ -111,6 +128,14 @@ select.todotxt-priority {
111128

112129
input.task-list-item-checkbox {
113130
margin-inline-end: 0.35rem !important;
131+
margin-inline-start: 0 !important;
132+
}
133+
134+
.is-phone input.task-list-item-checkbox {
135+
width: calc(var(--checkbox-size)*1.1) !important;
136+
height: calc(var(--checkbox-size)*1.1) !important;
137+
margin-inline-start: 0.2rem !important;
138+
margin-inline-end: 0.6rem !important;
114139
}
115140

116141
input[type="checkbox"]:checked~.todotxt-item-description,
@@ -270,6 +295,10 @@ button.mod-cta:disabled:hover {
270295
max-width: 100%;
271296
}
272297

298+
.todotxt-modal-input textarea {
299+
min-height: 4rem;
300+
}
301+
273302
.is-phone .todotxt-modal-input textarea {
274303
width: 100%;
275304
}

0 commit comments

Comments
 (0)