Skip to content

Commit ce5eed5

Browse files
authored
Merge pull request #2609 from DanielTMolloy919/deps-refactor-2
refactor: Task dependency modal inputs
2 parents e2211db + 2fa62f4 commit ce5eed5

File tree

4 files changed

+263
-358
lines changed

4 files changed

+263
-358
lines changed

src/ui/Dependency.svelte

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<script lang="ts">
2+
import type { Task } from "../Task/Task";
3+
import { computePosition, flip, offset, shift, size } from "@floating-ui/dom";
4+
import type { EditableTask } from "./EditableTask";
5+
6+
export let task: Task;
7+
export let editableTask: EditableTask;
8+
export let allTasks: Task[];
9+
export let _onDescriptionKeyDown: (e: KeyboardEvent) => void;
10+
export let type: "blocking" | "blockedBy";
11+
export let accesskey: (key: string) => string | null;
12+
13+
const MAX_SEARCH_RESULTS = 20;
14+
15+
let search: string = '';
16+
let searchResults: Task[] | null = null;
17+
let searchIndex: number | null = 0;
18+
let inputWidth: number;
19+
let inputFocused = false;
20+
let showDropdown = false;
21+
22+
let input: HTMLElement;
23+
let dropdown: HTMLElement;
24+
25+
function addTask(task: Task) {
26+
editableTask[type] = [...editableTask[type], task];
27+
search = '';
28+
inputFocused = false;
29+
}
30+
31+
function removeTask(task: Task) {
32+
editableTask[type] = editableTask[type].filter(item => item !== task);
33+
}
34+
35+
function taskKeydown(e: KeyboardEvent) {
36+
if (searchResults === null) return;
37+
38+
switch(e.key) {
39+
case "ArrowUp":
40+
e.preventDefault();
41+
if (!!searchIndex && searchIndex > 0) {
42+
searchIndex -= 1;
43+
} else {
44+
searchIndex = searchResults.length - 1;
45+
}
46+
break;
47+
case "ArrowDown":
48+
e.preventDefault();
49+
if (!!searchIndex && searchIndex < searchResults.length - 1) {
50+
searchIndex += 1;
51+
} else {
52+
searchIndex = 0;
53+
}
54+
break;
55+
case "Enter":
56+
if (searchIndex !== null) {
57+
e.preventDefault();
58+
addTask(searchResults[searchIndex]);
59+
searchIndex = null;
60+
inputFocused = false
61+
} else {
62+
_onDescriptionKeyDown(e);
63+
}
64+
break;
65+
default:
66+
searchIndex = 0;
67+
break;
68+
}
69+
searchIndex && dropdown?.getElementsByTagName('li')[searchIndex]?.scrollIntoView({ block: 'nearest' });
70+
}
71+
72+
function generateSearchResults(search: string) {
73+
if (!search && !showDropdown) return [];
74+
75+
showDropdown = false;
76+
77+
let results = allTasks.filter(task => task.description.toLowerCase().includes(search.toLowerCase()));
78+
79+
// remove itself, and tasks this task already has a relationship with from results
80+
results = results.filter((item) => {
81+
// line number is unavailable for the task being edited
82+
// Known issue - filters out duplicate lines in task file
83+
const sameFile = item.description === task.description &&
84+
item.taskLocation.path === task.taskLocation.path &&
85+
item.originalMarkdown === task.originalMarkdown
86+
87+
return ![...editableTask.blockedBy, ...editableTask.blocking].includes(item) && !sameFile;
88+
});
89+
90+
// search results favour tasks from the same file as this task
91+
results.sort((a, b) => {
92+
const aInSamePath = a.taskLocation.path === task.taskLocation.path;
93+
const bInSamePath = b.taskLocation.path === task.taskLocation.path;
94+
95+
// prioritise tasks close to this task in the same file
96+
if (aInSamePath && bInSamePath) {
97+
return Math.abs(a.taskLocation.lineNumber - task.taskLocation.lineNumber)
98+
- Math.abs(b.taskLocation.lineNumber - task.taskLocation.lineNumber);
99+
} else if (aInSamePath) {
100+
return -1;
101+
} else if (bInSamePath) {
102+
return 1;
103+
} else {
104+
return 0;
105+
}
106+
});
107+
108+
return results.slice(0,MAX_SEARCH_RESULTS);
109+
}
110+
111+
function onFocused() {
112+
inputFocused = true;
113+
showDropdown = true;
114+
}
115+
116+
function positionDropdown(input: HTMLElement, dropdown: HTMLElement) {
117+
if (!input || !dropdown) return;
118+
119+
computePosition(input, dropdown, {
120+
middleware: [
121+
offset(6),
122+
shift(),
123+
flip(),
124+
size({
125+
apply() {
126+
dropdown && Object.assign(dropdown.style, { width: `${inputWidth}px` });
127+
},
128+
}),
129+
],
130+
}).then(({ x, y }) => {
131+
dropdown.style.left = `${x}px`;
132+
dropdown.style.top = `${y}px`;
133+
});
134+
}
135+
136+
function displayPath(path: string) {
137+
return path === task.taskLocation.path ? "" : path;
138+
}
139+
140+
function showDescriptionTooltip(element: HTMLElement, text: string) {
141+
const tooltip = element.createDiv();
142+
tooltip.addClasses(['tooltip', 'pop-up']);
143+
tooltip.innerText = text;
144+
145+
computePosition(element, tooltip, {
146+
placement: "top",
147+
middleware: [
148+
offset(-18),
149+
shift()
150+
]
151+
}).then(({x, y}) => {
152+
tooltip.style.left = `${x}px`;
153+
tooltip.style.top = `${y}px`;
154+
});
155+
156+
element.addEventListener('mouseleave', () => tooltip.remove());
157+
}
158+
159+
$: {
160+
positionDropdown(input, dropdown);
161+
}
162+
163+
$: {
164+
searchResults = inputFocused ? generateSearchResults(search) : null;
165+
}
166+
</script>
167+
168+
<!-- svelte-ignore a11y-accesskey -->
169+
<span class="input" bind:clientWidth={inputWidth}>
170+
<input
171+
bind:this={input}
172+
bind:value={search}
173+
on:keydown={(e) => taskKeydown(e)}
174+
on:focus={onFocused}
175+
on:blur={() => inputFocused = false}
176+
accesskey={accesskey("b")}
177+
id="{type}"
178+
class="input"
179+
type="text"
180+
placeholder="Type to search..."
181+
/>
182+
</span>
183+
{#if searchResults && searchResults.length !== 0}
184+
<ul class="task-dependency-dropdown"
185+
bind:this={dropdown}
186+
on:mouseleave={() => searchIndex = null}>
187+
{#each searchResults as searchTask, index}
188+
{@const filepath = displayPath(searchTask.taskLocation.path)}
189+
<!-- svelte-ignore a11y-click-events-have-key-events -->
190+
<li on:mousedown={() => addTask(searchTask)}
191+
class:selected={search !== null && index === searchIndex}
192+
on:mouseenter={() => searchIndex = index}>
193+
<div class="{filepath ? 'dependency-name-shared' : 'dependency-name'}"
194+
on:mouseenter={(e) => showDescriptionTooltip(e.currentTarget, searchTask.descriptionWithoutTags)}>
195+
[{searchTask.status.symbol}] {searchTask.descriptionWithoutTags}
196+
</div>
197+
{#if filepath}
198+
<div class="dependency-path"
199+
on:mouseenter={(e) => showDescriptionTooltip(e.currentTarget, filepath)}>
200+
{filepath}
201+
</div>
202+
{/if}
203+
</li>
204+
{/each}
205+
</ul>
206+
{/if}
207+
<div class="task-dependencies-container results">
208+
{#each editableTask[type] as task}
209+
<div class="task-dependency"
210+
on:mouseenter={(e) => showDescriptionTooltip(e.currentTarget, task.descriptionWithoutTags)}>
211+
<span class="task-dependency-name">[{task.status.symbol}] {task.descriptionWithoutTags}</span>
212+
213+
<button on:click={() => removeTask(task)} type="button" class="task-dependency-delete">
214+
<svg style="display: block; margin: auto;" xmlns="http://www.w3.org/2000/svg" width="12"
215+
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"
216+
stroke-linejoin="round" class="lucide lucide-x">
217+
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
218+
</svg>
219+
</button>
220+
</div>
221+
{/each}
222+
</div>

0 commit comments

Comments
 (0)