@dvcol/neo-svelte
Version:
Neomorphic ui library for svelte 5
123 lines (122 loc) • 3.9 kB
JavaScript
import { getContext, setContext } from 'svelte';
import { SvelteMap } from 'svelte/reactivity';
import { NeoErrorMissingCollapseId } from '../utils/error.utils.js';
import { Logger } from '../utils/logger.utils.js';
export const NeoCollapseGroupStrategy = {
Readonly: 'readonly',
Oldest: 'oldest',
};
export const defaultCollapseGroupStrategy = {
min: NeoCollapseGroupStrategy.Readonly,
max: NeoCollapseGroupStrategy.Oldest,
};
/**
* Toggle the oldest changed sections.
* @param count - The number of sections to close
* @param fields - The fields in order of newest to oldest
* @param open - Whether to open or close the sections
*/
function toggleOldestChanged(count, fields, open = false) {
let nb = count;
let index = -1;
while (nb > 0 && fields.length >= Math.abs(index)) {
// Last changed section
const last = fields.at(index);
// If the last section is editable, close it
if (last && last.editable) {
last.open = open;
nb -= 1;
}
else {
// else, move to the next section
index -= 1;
}
}
}
export class NeoCollapseContext {
#group;
#map = new SvelteMap();
#opened = $derived([...this.#map.values()].filter(section => section.open).sort((a, b) => b.changed - a.changed));
#closed = $derived([...this.#map.values()].filter(section => !section.open).sort((a, b) => b.changed - a.changed));
get disabled() {
return this.#group.disabled;
}
get readonly() {
return this.#group.readonly;
}
get min() {
return this.#group.min;
}
get max() {
return this.#group.max;
}
get opened() {
return this.#opened.length;
}
get closed() {
return this.#closed.length;
}
get strategy() {
if (typeof this.#group.strategy === 'object')
return this.#group.strategy;
return {
min: this.#group.strategy,
max: this.#group.strategy,
};
}
get canOpen() {
if (this.strategy.max === NeoCollapseGroupStrategy.Oldest)
return true;
if (this.max)
return this.opened < this.max;
return true;
}
get canClose() {
if (this.strategy.min === NeoCollapseGroupStrategy.Oldest)
return true;
if (this.min)
return this.opened > this.min;
return true;
}
constructor(group) {
this.#group = group;
}
register(id, section) {
if (!id)
throw new NeoErrorMissingCollapseId();
if (this.#map.has(id)) {
return Logger.warn(`Section ID '${String(id)}' already exists. Tab registration ignored.`, {
existing: this.#map.get(id),
ignored: section,
groupId: this.#group.id,
});
}
this.#map.set(id, section);
this.#enforce();
}
remove(sectionId) {
this.#map.delete(sectionId);
}
#enforce() {
if (this.max && this.opened > this.max) {
toggleOldestChanged(this.opened - this.max, this.#opened);
}
else if (this.min && this.opened < this.min) {
toggleOldestChanged(this.min - this.opened, this.#closed, true);
}
}
update(sectionId) {
const section = this.#map.get(sectionId);
if (!section)
return Logger.warn(`Section ID '${String(sectionId)}' does not exist.`, { sectionId, groupId: this.#group.id });
section.changed = Date.now();
this.#enforce();
}
}
const NeoGroupContextSymbol = Symbol('NeoCollapseGroupContext');
export function getNeoCollapseGroupContext() {
return getContext(NeoGroupContextSymbol);
}
export function setCollapseGroupContext(group) {
return setContext(NeoGroupContextSymbol, new NeoCollapseContext(group));
}