Compare commits
No commits in common. "a6589fb1f311643bab13a8b6d925ca101ee56558" and "7a930228fc15b28002158606929162d07b12c825" have entirely different histories.
a6589fb1f3
...
7a930228fc
@ -31,7 +31,7 @@ src/
|
|||||||
|
|
||||||
## Key Design Decisions
|
## Key Design Decisions
|
||||||
|
|
||||||
- **Svelte 5 runes** — Use `$state`, `$derived`, `$effect`. Props-based event passing (no Svelte events). Event handler attributes are all lower-case (e.g. oninput)
|
- **Svelte 5 runes** — Use `$state`, `$derived`, `$effect`. Props-based event passing (no Svelte events).
|
||||||
- **No external crypto libraries** — Uses the browser's native Web Crypto API exclusively.
|
- **No external crypto libraries** — Uses the browser's native Web Crypto API exclusively.
|
||||||
- **Key never persisted** — Encryption key lives only in `$state` memory; cleared on lock, tab switch, or page close.
|
- **Key never persisted** — Encryption key lives only in `$state` memory; cleared on lock, tab switch, or page close.
|
||||||
- **Single-file build** — `vite-plugin-singlefile` inlines all JS/CSS; post-build script inlines favicon.
|
- **Single-file build** — `vite-plugin-singlefile` inlines all JS/CSS; post-build script inlines favicon.
|
||||||
|
|||||||
97
dist/index.html
vendored
97
dist/index.html
vendored
@ -2326,6 +2326,20 @@ function merge_text_nodes(text) {
|
|||||||
//#endregion
|
//#endregion
|
||||||
//#region node_modules/svelte/src/internal/client/dom/elements/misc.js
|
//#region node_modules/svelte/src/internal/client/dom/elements/misc.js
|
||||||
/**
|
/**
|
||||||
|
* @param {HTMLElement} dom
|
||||||
|
* @param {boolean} value
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function autofocus(dom, value) {
|
||||||
|
if (value) {
|
||||||
|
const body = document.body;
|
||||||
|
dom.autofocus = true;
|
||||||
|
queue_micro_task(() => {
|
||||||
|
if (document.activeElement === body) dom.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
* The child of a textarea actually corresponds to the defaultValue property, so we need
|
* The child of a textarea actually corresponds to the defaultValue property, so we need
|
||||||
* to remove it upon hydration to avoid a bug when someone resets the form value.
|
* to remove it upon hydration to avoid a bug when someone resets the form value.
|
||||||
* @param {HTMLTextAreaElement} dom
|
* @param {HTMLTextAreaElement} dom
|
||||||
@ -4178,36 +4192,6 @@ function head(hash, render_fn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region node_modules/svelte/src/internal/client/dom/elements/actions.js
|
|
||||||
/** @import { ActionPayload } from '#client' */
|
|
||||||
/**
|
|
||||||
* @template P
|
|
||||||
* @param {Element} dom
|
|
||||||
* @param {(dom: Element, value?: P) => ActionPayload<P>} action
|
|
||||||
* @param {() => P} [get_value]
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function action(dom, action, get_value) {
|
|
||||||
effect(() => {
|
|
||||||
var payload = untrack(() => action(dom, get_value?.()) || {});
|
|
||||||
if (get_value && payload?.update) {
|
|
||||||
var inited = false;
|
|
||||||
/** @type {P} */
|
|
||||||
var prev = {};
|
|
||||||
render_effect(() => {
|
|
||||||
var value = get_value();
|
|
||||||
deep_read_state(value);
|
|
||||||
if (inited && safe_not_equal(prev, value)) {
|
|
||||||
prev = value;
|
|
||||||
/** @type {Function} */ payload.update(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
inited = true;
|
|
||||||
}
|
|
||||||
if (payload?.destroy) return () => payload.destroy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
//#region node_modules/svelte/src/internal/shared/attributes.js
|
//#region node_modules/svelte/src/internal/shared/attributes.js
|
||||||
var whitespace = [..." \n\r\f\xA0\v"];
|
var whitespace = [..." \n\r\f\xA0\v"];
|
||||||
/**
|
/**
|
||||||
@ -5631,15 +5615,6 @@ async function importAll(data, mode = "merge", sourcePassword = "", targetKey =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/autofocus.js
|
|
||||||
/**
|
|
||||||
* Action that autofocuses an element after mount when the condition is truthy.
|
|
||||||
* Uses a microtask to ensure the element is fully rendered.
|
|
||||||
*/
|
|
||||||
function autofocus(node, condition = true) {
|
|
||||||
if (condition) queueMicrotask(() => node.focus());
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
//#region src/components/LockScreen.svelte
|
//#region src/components/LockScreen.svelte
|
||||||
var root_1$8 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-7sq1ct" role="alert"> </div>`);
|
var root_1$8 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-7sq1ct" role="alert"> </div>`);
|
||||||
var root_2$6 = /* @__PURE__ */ from_html(`<div class="form-group"><label for="confirm-password">Confirm Password</label> <input id="confirm-password" type="password" placeholder="Confirm master password" autocomplete="new-password"/></div>`);
|
var root_2$6 = /* @__PURE__ */ from_html(`<div class="form-group"><label for="confirm-password">Confirm Password</label> <input id="confirm-password" type="password" placeholder="Confirm master password" autocomplete="new-password"/></div>`);
|
||||||
@ -5722,8 +5697,7 @@ function LockScreen($$anchor, $$props) {
|
|||||||
var div_3 = child(form);
|
var div_3 = child(form);
|
||||||
var input = sibling(child(div_3), 2);
|
var input = sibling(child(div_3), 2);
|
||||||
remove_input_defaults(input);
|
remove_input_defaults(input);
|
||||||
effect(() => bind_value(input, () => get(masterPassword), ($$value) => set(masterPassword, $$value)));
|
autofocus(input, true);
|
||||||
action(input, ($$node) => autofocus?.($$node));
|
|
||||||
reset(div_3);
|
reset(div_3);
|
||||||
var node_1 = sibling(div_3, 2);
|
var node_1 = sibling(div_3, 2);
|
||||||
var consequent_1 = ($$anchor) => {
|
var consequent_1 = ($$anchor) => {
|
||||||
@ -5758,12 +5732,12 @@ function LockScreen($$anchor, $$props) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
});
|
});
|
||||||
|
bind_value(input, () => get(masterPassword), ($$value) => set(masterPassword, $$value));
|
||||||
append($$anchor, div);
|
append($$anchor, div);
|
||||||
pop();
|
pop();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/lib/stores/search.svelte.js
|
//#region src/lib/stores/search.svelte.js
|
||||||
var DEBOUNCE_MS = 300;
|
|
||||||
var SearchStore = class {
|
var SearchStore = class {
|
||||||
#query = /* @__PURE__ */ state("");
|
#query = /* @__PURE__ */ state("");
|
||||||
get query() {
|
get query() {
|
||||||
@ -5772,13 +5746,6 @@ var SearchStore = class {
|
|||||||
set query(value) {
|
set query(value) {
|
||||||
set(this.#query, value, true);
|
set(this.#query, value, true);
|
||||||
}
|
}
|
||||||
#debouncedQuery = /* @__PURE__ */ state("");
|
|
||||||
get debouncedQuery() {
|
|
||||||
return get(this.#debouncedQuery);
|
|
||||||
}
|
|
||||||
set debouncedQuery(value) {
|
|
||||||
set(this.#debouncedQuery, value, true);
|
|
||||||
}
|
|
||||||
#activeGroupId = /* @__PURE__ */ state("all");
|
#activeGroupId = /* @__PURE__ */ state("all");
|
||||||
get activeGroupId() {
|
get activeGroupId() {
|
||||||
return get(this.#activeGroupId);
|
return get(this.#activeGroupId);
|
||||||
@ -5793,21 +5760,8 @@ var SearchStore = class {
|
|||||||
set refreshTrigger(value) {
|
set refreshTrigger(value) {
|
||||||
set(this.#refreshTrigger, value, true);
|
set(this.#refreshTrigger, value, true);
|
||||||
}
|
}
|
||||||
#debounceTimer = null;
|
|
||||||
setSearchQuery(value) {
|
|
||||||
this.query = value;
|
|
||||||
if (this.#debounceTimer) clearTimeout(this.#debounceTimer);
|
|
||||||
if (value === "") {
|
|
||||||
this.debouncedQuery = "";
|
|
||||||
this.#debounceTimer = null;
|
|
||||||
} else this.#debounceTimer = setTimeout(() => {
|
|
||||||
this.debouncedQuery = value;
|
|
||||||
this.#debounceTimer = null;
|
|
||||||
}, DEBOUNCE_MS);
|
|
||||||
}
|
|
||||||
clear() {
|
clear() {
|
||||||
this.query = "";
|
this.query = "";
|
||||||
this.debouncedQuery = "";
|
|
||||||
this.activeGroupId = "all";
|
this.activeGroupId = "all";
|
||||||
}
|
}
|
||||||
/** Force subscribed components to re-fetch data. */
|
/** Force subscribed components to re-fetch data. */
|
||||||
@ -5959,8 +5913,6 @@ function Sidebar($$anchor, $$props) {
|
|||||||
var div_8 = sibling(node_2, 2);
|
var div_8 = sibling(node_2, 2);
|
||||||
var input_1 = sibling(child(div_8), 2);
|
var input_1 = sibling(child(div_8), 2);
|
||||||
remove_input_defaults(input_1);
|
remove_input_defaults(input_1);
|
||||||
effect(() => bind_value(input_1, () => get(groupName), ($$value) => set(groupName, $$value)));
|
|
||||||
action(input_1, ($$node, $$action_arg) => autofocus?.($$node, $$action_arg), () => !get(editingGroupId));
|
|
||||||
reset(div_8);
|
reset(div_8);
|
||||||
var div_9 = sibling(div_8, 2);
|
var div_9 = sibling(div_8, 2);
|
||||||
var div_10 = sibling(child(div_9), 2);
|
var div_10 = sibling(child(div_9), 2);
|
||||||
@ -5990,6 +5942,7 @@ function Sidebar($$anchor, $$props) {
|
|||||||
});
|
});
|
||||||
delegated("click", div_5, () => set(showGroupForm, false));
|
delegated("click", div_5, () => set(showGroupForm, false));
|
||||||
delegated("click", div_6, (e) => e.stopPropagation());
|
delegated("click", div_6, (e) => e.stopPropagation());
|
||||||
|
bind_value(input_1, () => get(groupName), ($$value) => set(groupName, $$value));
|
||||||
delegated("click", button_6, saveGroup);
|
delegated("click", button_6, saveGroup);
|
||||||
delegated("click", button_7, () => set(showGroupForm, false));
|
delegated("click", button_7, () => set(showGroupForm, false));
|
||||||
append($$anchor, div_5);
|
append($$anchor, div_5);
|
||||||
@ -6028,13 +5981,13 @@ function Sidebar($$anchor, $$props) {
|
|||||||
set_value(input, search.query);
|
set_value(input, search.query);
|
||||||
set_class(button, 1, `group-item ${search.activeGroupId === "all" ? "active" : ""}`, "svelte-181dlmc");
|
set_class(button, 1, `group-item ${search.activeGroupId === "all" ? "active" : ""}`, "svelte-181dlmc");
|
||||||
});
|
});
|
||||||
delegated("input", input, (e) => search.setSearchQuery(e.target.value));
|
event("Input", input, (e) => search.query = e.target.value);
|
||||||
delegated("click", button, () => search.activeGroupId = "all");
|
delegated("click", button, () => search.activeGroupId = "all");
|
||||||
delegated("click", button_4, () => openGroupForm(null));
|
delegated("click", button_4, () => openGroupForm(null));
|
||||||
append($$anchor, div);
|
append($$anchor, div);
|
||||||
pop();
|
pop();
|
||||||
}
|
}
|
||||||
delegate(["input", "click"]);
|
delegate(["click"]);
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/components/EntryList.svelte
|
//#region src/components/EntryList.svelte
|
||||||
var root_1$6 = /* @__PURE__ */ from_html(`<div class="loading svelte-13s7gu4">Loading entries...</div>`);
|
var root_1$6 = /* @__PURE__ */ from_html(`<div class="loading svelte-13s7gu4">Loading entries...</div>`);
|
||||||
@ -6055,7 +6008,7 @@ function EntryList($$anchor, $$props) {
|
|||||||
set(loading, true);
|
set(loading, true);
|
||||||
set(error, "");
|
set(error, "");
|
||||||
try {
|
try {
|
||||||
const query = search.debouncedQuery.trim();
|
const query = search.query.trim();
|
||||||
const groupId = search.activeGroupId;
|
const groupId = search.activeGroupId;
|
||||||
if (query) set(entries, await searchEntries(query, groupId !== "all" ? { groupId } : {}), true);
|
if (query) set(entries, await searchEntries(query, groupId !== "all" ? { groupId } : {}), true);
|
||||||
else if (groupId !== "all") set(entries, await getEntries({ groupId }), true);
|
else if (groupId !== "all") set(entries, await getEntries({ groupId }), true);
|
||||||
@ -6067,7 +6020,7 @@ function EntryList($$anchor, $$props) {
|
|||||||
set(loading, false);
|
set(loading, false);
|
||||||
}
|
}
|
||||||
user_effect(() => {
|
user_effect(() => {
|
||||||
search.debouncedQuery;
|
search.query;
|
||||||
search.activeGroupId;
|
search.activeGroupId;
|
||||||
search.refreshTrigger;
|
search.refreshTrigger;
|
||||||
loadEntries();
|
loadEntries();
|
||||||
@ -6541,8 +6494,6 @@ function EntryForm($$anchor, $$props) {
|
|||||||
var div_5 = sibling(node_2, 2);
|
var div_5 = sibling(node_2, 2);
|
||||||
var input = sibling(child(div_5), 2);
|
var input = sibling(child(div_5), 2);
|
||||||
remove_input_defaults(input);
|
remove_input_defaults(input);
|
||||||
effect(() => bind_value(input, () => get(title), ($$value) => set(title, $$value)));
|
|
||||||
action(input, ($$node, $$action_arg) => autofocus?.($$node, $$action_arg), () => !get(isEdit));
|
|
||||||
reset(div_5);
|
reset(div_5);
|
||||||
var div_6 = sibling(div_5, 2);
|
var div_6 = sibling(div_5, 2);
|
||||||
var input_1 = sibling(child(div_6), 2);
|
var input_1 = sibling(child(div_6), 2);
|
||||||
@ -6600,6 +6551,7 @@ function EntryForm($$anchor, $$props) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
});
|
});
|
||||||
|
bind_value(input, () => get(title), ($$value) => set(title, $$value));
|
||||||
bind_value(input_1, () => get(username), ($$value) => set(username, $$value));
|
bind_value(input_1, () => get(username), ($$value) => set(username, $$value));
|
||||||
bind_value(input_2, () => get(password), ($$value) => set(password, $$value));
|
bind_value(input_2, () => get(password), ($$value) => set(password, $$value));
|
||||||
delegated("click", button, () => set(passwordVisible, !get(passwordVisible)));
|
delegated("click", button, () => set(passwordVisible, !get(passwordVisible)));
|
||||||
@ -6904,7 +6856,10 @@ function MainLayout($$anchor, $$props) {
|
|||||||
if (get(sidebarOpen)) $$render(consequent);
|
if (get(sidebarOpen)) $$render(consequent);
|
||||||
});
|
});
|
||||||
var aside = sibling(node, 2);
|
var aside = sibling(node, 2);
|
||||||
Sidebar(child(aside), {});
|
Sidebar(child(aside), { $$events: {
|
||||||
|
back: handleBack,
|
||||||
|
goList
|
||||||
|
} });
|
||||||
reset(aside);
|
reset(aside);
|
||||||
var main = sibling(aside, 2);
|
var main = sibling(aside, 2);
|
||||||
var div_2 = child(main);
|
var div_2 = child(main);
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
import { generatePassword } from '../lib/crypto/crypto.js'
|
import { generatePassword } from '../lib/crypto/crypto.js'
|
||||||
import { app } from '../lib/stores/app.svelte.js'
|
import { app } from '../lib/stores/app.svelte.js'
|
||||||
import PasswordGenerator from './PasswordGenerator.svelte'
|
import PasswordGenerator from './PasswordGenerator.svelte'
|
||||||
import { autofocus } from '../lib/autofocus.js'
|
|
||||||
|
|
||||||
let { entryId, onSave, onCancel } = $props()
|
let { entryId, onSave, onCancel } = $props()
|
||||||
|
|
||||||
@ -115,7 +114,7 @@ import { autofocus } from '../lib/autofocus.js'
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">Title *</label>
|
<label for="title">Title *</label>
|
||||||
<input id="title" type="text" bind:value={title} placeholder="e.g. GitHub, Gmail" use:autofocus={!isEdit} />
|
<input id="title" type="text" bind:value={title} placeholder="e.g. GitHub, Gmail" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
loading = true
|
loading = true
|
||||||
error = ''
|
error = ''
|
||||||
try {
|
try {
|
||||||
const query = searchStore.debouncedQuery.trim()
|
const query = searchStore.query.trim()
|
||||||
const groupId = searchStore.activeGroupId
|
const groupId = searchStore.activeGroupId
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
@ -35,9 +35,9 @@
|
|||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload when debounced search query, active group filter, or refresh trigger changes
|
// Reload when search query, active group filter, or refresh trigger changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
searchStore.debouncedQuery
|
searchStore.query
|
||||||
searchStore.activeGroupId
|
searchStore.activeGroupId
|
||||||
searchStore.refreshTrigger
|
searchStore.refreshTrigger
|
||||||
loadEntries()
|
loadEntries()
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js'
|
import { deriveKey, createTestPayload, verifyPassword } from '../lib/crypto/crypto.js'
|
||||||
import { saveVaultMeta, loadVaultMeta, isVaultInitialized } from '../lib/storage/db.js'
|
import { saveVaultMeta, loadVaultMeta, isVaultInitialized } from '../lib/storage/db.js'
|
||||||
import { startAutoLock } from '../lib/stores/security.svelte.js'
|
import { startAutoLock } from '../lib/stores/security.svelte.js'
|
||||||
import { autofocus } from '../lib/autofocus.js'
|
|
||||||
|
|
||||||
let masterPassword = $state('')
|
let masterPassword = $state('')
|
||||||
let confirmPassword = $state('')
|
let confirmPassword = $state('')
|
||||||
@ -96,7 +95,7 @@
|
|||||||
bind:value={masterPassword}
|
bind:value={masterPassword}
|
||||||
placeholder="Enter master password"
|
placeholder="Enter master password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
use:autofocus
|
autofocus
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar {sidebarOpen ? 'open' : ''}">
|
<aside class="sidebar {sidebarOpen ? 'open' : ''}">
|
||||||
<Sidebar />
|
<Sidebar on:back={handleBack} on:goList={goList} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
import { getGroups, addGroup, updateGroup, deleteGroup, getEntryCountsByGroup } from '../lib/storage/db.js'
|
import { getGroups, addGroup, updateGroup, deleteGroup, getEntryCountsByGroup } from '../lib/storage/db.js'
|
||||||
import { createGroup, validateGroup } from '../lib/models/schema.js'
|
import { createGroup, validateGroup } from '../lib/models/schema.js'
|
||||||
import { search as searchStore } from '../lib/stores/search.svelte.js'
|
import { search as searchStore } from '../lib/stores/search.svelte.js'
|
||||||
import { autofocus } from '../lib/autofocus.js'
|
|
||||||
|
|
||||||
let groups = $state([])
|
let groups = $state([])
|
||||||
let entryCounts = $state(new Map())
|
let entryCounts = $state(new Map())
|
||||||
@ -96,7 +95,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search entries..."
|
placeholder="Search entries..."
|
||||||
value={searchStore.query}
|
value={searchStore.query}
|
||||||
oninput={(e) => searchStore.setSearchQuery(e.target.value)}
|
onInput={(e) => searchStore.query = e.target.value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -143,7 +142,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="group-name">Group Name</label>
|
<label for="group-name">Group Name</label>
|
||||||
<input id="group-name" type="text" bind:value={groupName} placeholder="e.g. Work, Personal" use:autofocus={!editingGroupId} />
|
<input id="group-name" type="text" bind:value={groupName} placeholder="e.g. Work, Personal" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Action that autofocuses an element after mount when the condition is truthy.
|
|
||||||
* Uses a microtask to ensure the element is fully rendered.
|
|
||||||
*/
|
|
||||||
export function autofocus(node, condition = true) {
|
|
||||||
if (condition) {
|
|
||||||
queueMicrotask(() => node.focus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,36 +3,13 @@
|
|||||||
* Shared between Sidebar and EntryList for coordinated filtering.
|
* Shared between Sidebar and EntryList for coordinated filtering.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEBOUNCE_MS = 300
|
|
||||||
|
|
||||||
export class SearchStore {
|
export class SearchStore {
|
||||||
query = $state('') // raw input value — bound to the search input
|
query = $state('')
|
||||||
debouncedQuery = $state('') // debounced value — used for actual search
|
|
||||||
activeGroupId = $state('all') // 'all' or a group id
|
activeGroupId = $state('all') // 'all' or a group id
|
||||||
refreshTrigger = $state(0) // incremented to force a re-fetch
|
refreshTrigger = $state(0) // incremented to force a re-fetch
|
||||||
#debounceTimer = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the search query with debouncing.
|
|
||||||
* Call this from the input handler instead of setting `query` directly.
|
|
||||||
*/
|
|
||||||
setSearchQuery(value) {
|
|
||||||
this.query = value
|
|
||||||
if (this.#debounceTimer) clearTimeout(this.#debounceTimer)
|
|
||||||
if (value === '') {
|
|
||||||
this.debouncedQuery = ''
|
|
||||||
this.#debounceTimer = null
|
|
||||||
} else {
|
|
||||||
this.#debounceTimer = setTimeout(() => {
|
|
||||||
this.debouncedQuery = value
|
|
||||||
this.#debounceTimer = null
|
|
||||||
}, DEBOUNCE_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.query = ''
|
this.query = ''
|
||||||
this.debouncedQuery = ''
|
|
||||||
this.activeGroupId = 'all'
|
this.activeGroupId = 'all'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,12 +18,12 @@ describe('SearchStore', () => {
|
|||||||
|
|
||||||
describe('query', () => {
|
describe('query', () => {
|
||||||
it('should update query', () => {
|
it('should update query', () => {
|
||||||
search.setSearchQuery('test')
|
search.query = 'test'
|
||||||
expect(search.query).toBe('test')
|
expect(search.query).toBe('test')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle unicode query', () => {
|
it('should handle unicode query', () => {
|
||||||
search.setSearchQuery('тест')
|
search.query = 'тест'
|
||||||
expect(search.query).toBe('тест')
|
expect(search.query).toBe('тест')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -48,7 +48,7 @@ describe('SearchStore', () => {
|
|||||||
|
|
||||||
describe('clear()', () => {
|
describe('clear()', () => {
|
||||||
it('should reset query to empty string', () => {
|
it('should reset query to empty string', () => {
|
||||||
search.setSearchQuery('some search')
|
search.query = 'some search'
|
||||||
search.clear()
|
search.clear()
|
||||||
expect(search.query).toBe('')
|
expect(search.query).toBe('')
|
||||||
})
|
})
|
||||||
@ -60,7 +60,7 @@ describe('SearchStore', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should reset both fields simultaneously', () => {
|
it('should reset both fields simultaneously', () => {
|
||||||
search.setSearchQuery('some search')
|
search.query = 'some search'
|
||||||
search.activeGroupId = 'group-123'
|
search.activeGroupId = 'group-123'
|
||||||
search.clear()
|
search.clear()
|
||||||
expect(search.query).toBe('')
|
expect(search.query).toBe('')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user