|
1 | 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"; |
| 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 | 5 |
|
6 | 6 | export let task: Task;
|
7 | 7 | export let editableTask: EditableTask;
|
|
10 | 10 | export let type: "blocking" | "blockedBy";
|
11 | 11 | export let accesskey: (key: string) => string | null;
|
12 | 12 |
|
13 |
| - let existingTasks = type === "blocking" ? editableTask.blocking : editableTask.blockedBy; |
14 |
| -
|
15 |
| - console.log(`blockedBy: ${editableTask.blockedBy.map(task => task.description)}`) |
| 13 | + const MAX_SEARCH_RESULTS = 20; |
16 | 14 |
|
17 | 15 | let search: string = '';
|
18 | 16 | let searchResults: Task[] | null = null;
|
19 | 17 | let searchIndex: number | null = 0;
|
20 |
| -
|
21 |
| - let depInputWidth: number; |
22 |
| -
|
| 18 | + let inputWidth: number; |
23 | 19 | let inputFocused = false;
|
| 20 | + let showDropdown = false; |
24 | 21 |
|
25 |
| - let displayResultsIfSearchEmpty = false; |
26 |
| -
|
27 |
| - function onFocused() { |
28 |
| - inputFocused = true; |
29 |
| - displayResultsIfSearchEmpty = true; |
30 |
| - } |
31 |
| -
|
32 |
| -
|
33 |
| - let dependencyInput: HTMLElement; |
34 |
| - let dependencyDropdown: HTMLElement; |
| 22 | + let input: HTMLElement; |
| 23 | + let dropdown: HTMLElement; |
35 | 24 |
|
36 |
| - function positionDropdown(ref: HTMLElement, content: HTMLElement) { |
37 |
| - if (!ref || !content) return; |
38 |
| -
|
39 |
| - computePosition(ref, content, { |
40 |
| - middleware: [ |
41 |
| - offset(6), |
42 |
| - shift(), |
43 |
| - flip(), |
44 |
| - size({ |
45 |
| - apply() { |
46 |
| - content && Object.assign(content.style, { width: `${depInputWidth}px` }); |
47 |
| - }, |
48 |
| - }), |
49 |
| - ], |
50 |
| - }).then(({ x, y }) => { |
51 |
| - Object.assign(content.style, { |
52 |
| - left: `${x}px`, |
53 |
| - top: `${y}px`, |
54 |
| - }); |
55 |
| - }); |
56 |
| - } |
57 |
| -
|
58 |
| - const _displayableFilePath = (path: string) => { |
59 |
| - if (path === task.taskLocation.path) return ""; |
60 |
| -
|
61 |
| - return path.slice(0,-3); |
62 |
| - } |
63 |
| -
|
64 |
| - $: { |
65 |
| - positionDropdown(dependencyInput, dependencyDropdown); |
| 25 | + function addTask(task: Task) { |
| 26 | + editableTask[type] = [...editableTask[type], task]; |
| 27 | + search = ''; |
| 28 | + inputFocused = false; |
66 | 29 | }
|
67 | 30 |
|
68 |
| - $: { |
69 |
| - searchResults = inputFocused ? generateSearchResults(search) : null; |
| 31 | + function removeTask(task: Task) { |
| 32 | + editableTask[type] = editableTask[type].filter(item => item !== task); |
70 | 33 | }
|
71 | 34 |
|
72 | 35 | function taskKeydown(e: KeyboardEvent) {
|
|
75 | 38 | switch(e.key) {
|
76 | 39 | case "ArrowUp":
|
77 | 40 | e.preventDefault();
|
78 |
| - if (searchIndex === 0 || searchIndex === null) { |
79 |
| - searchIndex = searchResults.length - 1; |
80 |
| - } else { |
| 41 | + if (!!searchIndex && searchIndex > 0) { |
81 | 42 | searchIndex -= 1;
|
| 43 | + } else { |
| 44 | + searchIndex = searchResults.length - 1; |
82 | 45 | }
|
83 | 46 | break;
|
84 | 47 | case "ArrowDown":
|
85 | 48 | e.preventDefault();
|
86 |
| - if (searchIndex === searchResults.length - 1 || searchIndex === null) { |
87 |
| - searchIndex = 0; |
88 |
| - } else { |
| 49 | + if (!!searchIndex && searchIndex < searchResults.length - 1) { |
89 | 50 | searchIndex += 1;
|
| 51 | + } else { |
| 52 | + searchIndex = 0; |
90 | 53 | }
|
91 | 54 | break;
|
92 | 55 | case "Enter":
|
|
103 | 66 | searchIndex = 0;
|
104 | 67 | break;
|
105 | 68 | }
|
106 |
| - searchIndex = searchIndex; |
107 |
| - if (searchIndex !== null) { |
108 |
| - dependencyDropdown?.getElementsByTagName('li')[searchIndex]?.scrollIntoView(false) |
109 |
| - } |
110 |
| - } |
111 |
| -
|
112 |
| - function showDescriptionTooltip(element: HTMLElement, text: string) { |
113 |
| - const tooltip = element.createDiv(); |
114 |
| - tooltip.addClasses(['tooltip', 'pop-up']); |
115 |
| - tooltip.innerText = text; |
116 |
| -
|
117 |
| - computePosition(element, tooltip, { |
118 |
| - placement: "top", |
119 |
| - middleware: [ |
120 |
| - offset(-18), |
121 |
| - shift() |
122 |
| - ] |
123 |
| - }).then(({x, y}) => { |
124 |
| - Object.assign(tooltip.style, { |
125 |
| - left: `${x}px`, |
126 |
| - top: `${y}px`, |
127 |
| - }); |
128 |
| - }); |
129 |
| -
|
130 |
| - element.addEventListener('mouseleave', () => { |
131 |
| - tooltip.remove(); |
132 |
| - }); |
| 69 | + searchIndex && dropdown?.getElementsByTagName('li')[searchIndex]?.scrollIntoView({ block: 'nearest' }); |
133 | 70 | }
|
134 | 71 |
|
135 |
| -
|
136 | 72 | function generateSearchResults(search: string) {
|
137 |
| - if (!search && !displayResultsIfSearchEmpty) return []; |
| 73 | + if (!search && !showDropdown) return []; |
138 | 74 |
|
139 |
| - displayResultsIfSearchEmpty = false; |
| 75 | + showDropdown = false; |
140 | 76 |
|
141 | 77 | let results = allTasks.filter(task => task.description.toLowerCase().includes(search.toLowerCase()));
|
142 | 78 |
|
|
169 | 105 | }
|
170 | 106 | });
|
171 | 107 |
|
172 |
| - return results.slice(0,20); |
| 108 | + return results.slice(0,MAX_SEARCH_RESULTS); |
173 | 109 | }
|
174 | 110 |
|
175 |
| - function addTask(task: Task) { |
176 |
| - existingTasks = [...existingTasks, task]; |
177 |
| - if (type === "blocking") { |
178 |
| - editableTask.blocking = existingTasks; |
179 |
| - } else { |
180 |
| - editableTask.blockedBy = existingTasks; |
181 |
| - } |
182 |
| - search = ''; |
183 |
| - inputFocused = false; |
| 111 | + function onFocused() { |
| 112 | + inputFocused = true; |
| 113 | + showDropdown = true; |
184 | 114 | }
|
185 | 115 |
|
186 |
| - function removeTask(task: Task) { |
187 |
| - existingTasks = existingTasks.filter(item => item !== task); |
188 |
| - if (type === "blocking") { |
189 |
| - editableTask.blocking = existingTasks; |
190 |
| - } else { |
191 |
| - editableTask.blockedBy = existingTasks; |
192 |
| - } |
| 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; |
193 | 165 | }
|
194 | 166 | </script>
|
195 | 167 |
|
196 | 168 | <!-- svelte-ignore a11y-accesskey -->
|
197 |
| - |
198 |
| -<span class="input" bind:clientWidth={depInputWidth}> |
| 169 | +<span class="input" bind:clientWidth={inputWidth}> |
199 | 170 | <input
|
200 |
| - bind:this={dependencyInput} |
| 171 | + bind:this={input} |
201 | 172 | bind:value={search}
|
202 | 173 | on:keydown={(e) => taskKeydown(e)}
|
203 | 174 | on:focus={onFocused}
|
|
211 | 182 | </span>
|
212 | 183 | {#if searchResults && searchResults.length !== 0}
|
213 | 184 | <ul class="suggested-tasks"
|
214 |
| - bind:this={dependencyDropdown} |
| 185 | + bind:this={dropdown} |
215 | 186 | on:mouseleave={() => searchIndex = null}>
|
216 | 187 | {#each searchResults as searchTask, index}
|
217 |
| - {@const filepath = _displayableFilePath(searchTask.taskLocation.path)} |
| 188 | + {@const filepath = displayPath(searchTask.taskLocation.path)} |
218 | 189 | <!-- svelte-ignore a11y-click-events-have-key-events -->
|
219 | 190 | <li on:mousedown={() => addTask(searchTask)}
|
220 | 191 | class:selected={search !== null && index === searchIndex}
|
|
234 | 205 | </ul>
|
235 | 206 | {/if}
|
236 | 207 | <div class="chip-container results">
|
237 |
| - {#each existingTasks as task} |
| 208 | + {#each editableTask[type] as task} |
238 | 209 | <div class="chip"
|
239 | 210 | on:mouseenter={(e) => showDescriptionTooltip(e.currentTarget, task.descriptionWithoutTags)}>
|
240 | 211 | <span class="chip-name">[{task.status.symbol}] {task.descriptionWithoutTags}</span>
|
241 | 212 |
|
242 | 213 | <button on:click={() => removeTask(task)} type="button" class="chip-close">
|
243 |
| - <svg style="display: block; margin: auto;" xmlns="http://www.w3.org/2000/svg" width="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> |
| 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> |
244 | 219 | </button>
|
245 | 220 | </div>
|
246 | 221 | {/each}
|
|
0 commit comments