Skip to content

Commit

Permalink
Fix cursor movement behavior for section changing + add option for it #…
Browse files Browse the repository at this point in the history
  • Loading branch information
mgsloan committed Aug 8, 2022
1 parent d945018 commit 6ed4a60
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 37 deletions.
6 changes: 6 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ function todoistShortcutsLoadOptions(handleOptions, handleError) {
const FOCUS_FOLLOWS_MOUSE = 'focus-follows-mouse';
const NO_MOUSE_BEHAVIOR = 'no-mouse-behavior';

const CURSOR_MOVEMENT = 'cursor-movement';
const FOLLOWS_TASK_WITHIN_SECTION = 'follows-task-within-section';

try {
chrome.storage.sync.get({options: '{}'}, (storedValues) => {
const serializedOptions = storedValues['options'];
Expand Down Expand Up @@ -42,6 +45,9 @@ function todoistShortcutsLoadOptions(handleOptions, handleError) {
options[MOUSE_BEHAVIOR] = FOCUS_FOLLOWS_MOUSE;
changed = true;
}
if (!(CURSOR_MOVEMENT in options)) {
options[CURSOR_MOVEMENT] = FOLLOWS_TASK_WITHIN_SECTION;
}

// Save if canonicalization changed the options
if (changed) {
Expand Down
2 changes: 1 addition & 1 deletion src/options-page.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ body {
font-style: italic;
}

.mouse-behavior-option {
.option {
margin-left: 2em;
}
31 changes: 27 additions & 4 deletions src/options-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,46 @@ <h2>Settings</h2>
<p>
Mouse hover behavior:
</p>
<p class="mouse-behavior-option">
<p class="option">
<input type="radio" name="mouse-behavior" id="focus-follows-mouse">
<label for="focus-follows-mouse">
Mouse hover over task moves cursor to task
Mouse hover over task moves cursor to task (default)
</label>
<p class="mouse-behavior-option">
<p class="option">
<input type="radio" name="mouse-behavior" id="focus-follows-mouse-delay-after-window-focus">
<label for="focus-follows-mouse-delay-after-window-focus">
Mouse hover over task moves cursor to task, but not within 0.5 seconds after browser window focused
</label>
</p>
<p class="mouse-behavior-option">
<p class="option">
<input type="radio" name="mouse-behavior" id="no-mouse-behavior">
<label for="no-mouse-behavior">
Ignore mouse hover
</label>
</p>
<p>
<b>Cursor movement:</b> When the task under the cursor changes position (usually due to editing),
there are a variety of options for how this is handled:
</p>
<p class="option">
<input type="radio" name="cursor-movement" id="follows-task-within-section">
<label for="follows-task-within-section">
Cursor follows task unless it has changed to a different section (default)
</label>
<p class="option">
<input type="radio" name="cursor-movement" id="always-follows-task">
<label for="always-follows-task">
Cursor always follows the task if it is still in the current view
</label>
</p>
<!-- Implementing this turned out to be tricky
<p class="option">
<input type="radio" name="cursor-movement" id="no-follow-task">
<label for="no-follow-task">
Cursor does not follow the task, instead moves to an adjacent task
</label>
</p>
-->
<p id="status"></p>
</body>
</html>
22 changes: 22 additions & 0 deletions src/options-page.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
document.addEventListener('DOMContentLoaded', () => {
const MOUSE_BEHAVIOR = 'mouse-behavior';
const CURSOR_MOVEMENT = 'cursor-movement';

const mouseBehaviorOptionEls =
document.querySelectorAll('input[name="mouse-behavior"]');
const cursorMovementOptionEls =
document.querySelectorAll('input[name="cursor-movement"]');
const statusEl = document.getElementById('status');
let clearStatusTimeout = null;

Expand All @@ -14,6 +17,12 @@ document.addEventListener('DOMContentLoaded', () => {
break;
}
}
for (const radioButton of cursorMovementOptionEls) {
if (radioButton.checked) {
options[CURSOR_MOVEMENT] = radioButton.id;
break;
}
}
todoistShortcutsSaveOptions(options, () => {
statusEl.textContent = 'Changes saved.';
if (clearStatusTimeout) {
Expand All @@ -38,6 +47,19 @@ document.addEventListener('DOMContentLoaded', () => {
if (!foundMouseBehaviorOption) {
console.warn('No mouse behavior option matching ' + mouseBehavior);
}

const cursorMovement = options[CURSOR_MOVEMENT];
let foundCursorMovementOption = false;
for (const radioButton of cursorMovementOptionEls) {
radioButton.addEventListener('click', save);
if (radioButton.id === cursorMovement) {
radioButton.checked = true;
foundCursorMovementOption = true;
}
}
if (!foundCursorMovementOption) {
console.warn('No cursor movement option matching ' + cursorMovement);
}
}

todoistShortcutsLoadOptions(initialize, (e) => {
Expand Down
109 changes: 77 additions & 32 deletions src/todoist-shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

(function() {
// Set this to true to get more log output.
const DEBUG = false;
const DEBUG = true;

const IS_CHROME =
/Chrom/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
Expand Down Expand Up @@ -335,6 +335,14 @@
return result;
}

function getCursorMovementOption() {
const result = options['cursor-movement'];
if (!result) {
return 'follows-task-within-section';
}
return result;
}

/*****************************************************************************
* Action combiners
*/
Expand Down Expand Up @@ -1829,7 +1837,10 @@
// the cursor doesn't follow the task for these moves, hence this logic.
let changedSection = false;
let currentSection = null;
if (cursor && lastCursorType === TYPE_NORMAL) {
const cursorMovement = getCursorMovementOption();
if (cursor &&
cursorMovement === 'follows-task-within-section' &&
lastCursorType === TYPE_NORMAL) {
const cursorId = getTaskId(cursor);
const cursorIndent = getIndentClass(cursor);
if (lastCursorId === cursorId && lastCursorIndent === cursorIndent) {
Expand Down Expand Up @@ -1879,15 +1890,18 @@
tasks = getTasks();
for (let i = 0; i < tasks.length; i++) {
if (tasks[i] === taskPrecedingEditor && i + 1 < tasks.length) {
debug('found task after task that preceded the editor.');
setCursor(tasks[i + 1], 'no-scroll');
found = true;
found = restoreCursor(tasks[i + 1], 'no-scroll');
if (found) {
debug('found task after task that preceded the editor.');
}
break;
}
}
if (!found) {
debug('falling back on selecting task that preceded editor.');
setCursor(taskPrecedingEditor, 'no-scroll');
found = restoreCursor(taskPrecedingEditor, 'no-scroll');
if (found) {
debug('falling back on selecting task that preceded editor.');
}
}
} else {
warn('expected to find task that was being edited.');
Expand All @@ -1896,9 +1910,10 @@
const task =
getTaskById(lastCursorId, 'ignore-indent', lastCursorSection);
if (task) {
debug('found task that was being explicitly edited.');
found = true;
setCursor(task, 'scroll');
found = restoreCursor(task, 'scroll');
if (found) {
debug('found task that was being explicitly edited.');
}
}
} else if (lastCursorType == TYPE_NORMAL) {
for (let i = lastCursorIndex; i < lastCursorTasks.length; i++) {
Expand All @@ -1907,17 +1922,16 @@
const oldTaskId = getTaskId(oldTask);
task = getTaskById(oldTaskId, 'ignore-indent' );
if (task) {
const taskSection = getSectionName(task);
// Don't jump back to the same task if it changed section.
if (i !== lastCursorIndex || taskSection === lastCursorSection) {
debug(
if (i !== lastCursorIndex) {
found = restoreCursor(task, 'no-scroll');
if (found) {
debug(
'found still-existing task that is',
i - lastCursorIndex,
'tasks after old cursor position, at',
lastCursorIndex,
', setting cursor to it');
found = true;
setCursor(task, 'no-scroll');
', set cursor to it');
}
break;
} else {
debug('cursor changed section, finding new location');
Expand All @@ -1932,32 +1946,63 @@
debug('lastCursorIndex wasn\'t set yet');
}
if (!found) {
debug('didn\'t find a particular task to select.');
if (!tasks) {
tasks = getTasks();
}
if (lastCursorIndex < tasks.length - lastCursorIndex) {
debug('cursoring first task, because it\'s nearer to lastCursorIndex.');
debug('didn\'t find a particular task to select, so selecting by index.');
restoreCursorViaIndex(tasks);
}
}

function restoreCursorViaIndex(tasks) {
if (!tasks) {
tasks = getTasks();
}

// Attempt to adjust index if cursored task is now earlier in the list.
let indexBasedOnLastCursor = lastCursorIndex;
const foundLastTaskEl = getTaskById(lastCursorId, 'ignore-indent', lastCursorSection);
const foundLastTaskIndex = tasks.indexOf(foundLastTaskEl);
if (foundLastTaskIndex !== -1 && foundLastTaskIndex < indexBasedOnLastCursor) {
indexBasedOnLastCursor += 1;
}

if (0 <= indexBasedOnLastCursor && indexBasedOnLastCursor < tasks.length) {
debug('cursoring to index', indexBasedOnLastCursor);
setCursor(tasks[indexBasedOnLastCursor] , 'no-scroll');
} else if (lastCursorIndex < tasks.length - lastCursorIndex) {
debug('cursoring first task, because it\'s nearer to lastCursorIndex.');
setCursorToFirstTask('no-scroll');
} else {
debug('cursoring last task, because it\'s nearer to lastCursorIndex.');
setCursorToLastTask('no-scroll');
if (!getCursor()) {
// This can happen if the last task is a nested sub-project.
debug('failed to set the cursor to last task, so setting to first');
setCursorToFirstTask('no-scroll');
} else {
debug('cursoring last task, because it\'s nearer to lastCursorIndex.');
setCursorToLastTask('no-scroll');
if (!getCursor()) {
// This can happen if the last task is a nested sub-project.
debug('failed to set the cursor to last task, so setting to first');
setCursorToFirstTask('no-scroll');
}
}
}
}

function restoreCursor(task, scroll) {
// Don't jump back to the same task if it changed section and
// cursor movement setting does not follow across sections
const taskSection = getSectionName(task);
const cursorMovement = getCursorMovementOption();
let changedSection =
cursorMovement === 'follows-task-within-section' &&
lastCursorSection !== taskSection;
if (!changedSection) {
setCursor(task, scroll);
return true;
}
return false;
}

// Gets the name of the section that a task is in.
function getSectionName(task) {
const section = getSection(task);
if (!section) {
error('Failed to find section div for', task);
} else if ('aria-label' in section.attributes) {
return section.attributes['aria-label'];
return section.attributes['aria-label'].value;
} else {
error('Section', section, 'lacks an aria-label attribute');
}
Expand Down

0 comments on commit 6ed4a60

Please sign in to comment.