5e-tools/js/filter/filter-box.js
TheGiddyLimit 2eeeb0771b v1.209.0
2024-07-10 20:47:40 +01:00

827 lines
28 KiB
JavaScript

import {EVNT_VALCHANGE, SOURCE_HEADER, SUB_HASH_PREFIX_LENGTH, TITLE_BTN_RESET} from "./filter-constants.js";
import {FilterRegistry} from "./filter-registry.js";
export class FilterBox extends ProxyBase {
static selectFirstVisible (entryList) {
if (Hist.lastLoadedId == null && !Hist.initialLoad) {
Hist._freshLoad();
}
// This version deemed too annoying to be of practical use
// Instead of always loading the URL, this would switch to the first visible item that matches the filter
/*
if (Hist.lastLoadedId && !Hist.initialLoad) {
const last = entryList[Hist.lastLoadedId];
const lastHash = UrlUtil.autoEncodeHash(last);
const link = $("#listcontainer").find(`.list a[href="#${lastHash.toLowerCase()}"]`);
if (!link.length) Hist._freshLoad();
} else if (Hist.lastLoadedId == null && !Hist.initialLoad) {
Hist._freshLoad();
}
*/
}
/**
* @param opts Options object.
* @param [opts.$wrpFormTop] Form input group.
* @param opts.$btnReset Form reset button.
* @param [opts.$btnOpen] A custom button to use to open the filter overlay.
* @param [opts.$iptSearch] Search input associated with the "form" this filter is a part of. Only used for passing
* through search terms in @filter tags.
* @param [opts.$wrpMiniPills] Element to house mini pills.
* @param [opts.$btnToggleSummaryHidden] Button which toggles the filter summary.
* @param opts.filters Array of filters to be included in this box.
* @param [opts.isCompact] True if this box should have a compact/reduced UI.
* @param [opts.namespace] Namespace for this filter, to prevent collisions with other filters on the same page.
*/
constructor (opts) {
super();
this._$iptSearch = opts.$iptSearch;
this._$wrpFormTop = opts.$wrpFormTop;
this._$btnReset = opts.$btnReset;
this._$btnOpen = opts.$btnOpen;
this._$wrpMiniPills = opts.$wrpMiniPills;
this._$btnToggleSummaryHidden = opts.$btnToggleSummaryHidden;
this._filters = opts.filters;
this._isCompact = opts.isCompact;
this._namespace = opts.namespace;
this._doSaveStateThrottled = MiscUtil.throttle(() => this._pDoSaveState(), 50);
this.__meta = this._getDefaultMeta();
if (this._isCompact) this.__meta.isSummaryHidden = true;
this._meta = this._getProxy("meta", this.__meta);
this.__minisHidden = {};
this._minisHidden = this._getProxy("minisHidden", this.__minisHidden);
this.__combineAs = {};
this._combineAs = this._getProxy("combineAs", this.__combineAs);
this._modalMeta = null;
this._isRendered = false;
this._cachedState = null;
this._compSearch = BaseComponent.fromObject({search: ""});
this._metaIptSearch = null;
this._filters.forEach(f => f.filterBox = this);
this._eventListeners = {};
}
get filters () { return this._filters; }
teardown () {
this._filters.forEach(f => f._doTeardown());
if (this._modalMeta) this._modalMeta.doTeardown();
}
// region Event listeners
on (identifier, fn) {
const [eventName, namespace] = identifier.split(".");
(this._eventListeners[eventName] = this._eventListeners[eventName] || []).push({namespace, fn});
return this;
}
off (identifier, fn = null) {
const [eventName, namespace] = identifier.split(".");
this._eventListeners[eventName] = (this._eventListeners[eventName] || []).filter(it => {
if (fn != null) return it.namespace !== namespace || it.fn !== fn;
return it.namespace !== namespace;
});
if (!this._eventListeners[eventName].length) delete this._eventListeners[eventName];
return this;
}
fireChangeEvent () {
this._doSaveStateThrottled();
this.fireEvent(EVNT_VALCHANGE);
}
fireEvent (eventName) {
(this._eventListeners[eventName] || []).forEach(it => it.fn());
}
// endregion
_getNamespacedStorageKey () { return `${FilterBox._STORAGE_KEY}${this._namespace ? `.${this._namespace}` : ""}`; }
getNamespacedHashKey (k) { return `${k || "_".repeat(SUB_HASH_PREFIX_LENGTH)}${this._namespace ? `.${this._namespace}` : ""}`; }
async pGetStoredActiveSources () {
const stored = await StorageUtil.pGetForPage(this._getNamespacedStorageKey());
if (stored) {
const sourceFilterData = stored.filters[SOURCE_HEADER];
if (sourceFilterData) {
const state = sourceFilterData.state;
const blue = [];
const white = [];
Object.entries(state).forEach(([src, mode]) => {
if (mode === 1) blue.push(src);
else if (mode !== -1) white.push(src);
});
if (blue.length) return blue; // if some are selected, we load those
else return white; // otherwise, we load non-red
}
}
return null;
}
registerMinisHiddenHook (prop, hook) {
this._addHook("minisHidden", prop, hook);
}
isMinisHidden (header) {
return !!this._minisHidden[header];
}
async pDoLoadState () {
const toLoad = await StorageUtil.pGetForPage(this._getNamespacedStorageKey());
if (toLoad == null) return;
this._setStateFromLoaded(toLoad, {isUserSavedState: true});
}
_setStateFromLoaded (state, {isUserSavedState = false} = {}) {
state.box = state.box || {};
this._proxyAssign("meta", "_meta", "__meta", state.box.meta || {}, true);
this._proxyAssign("minisHidden", "_minisHidden", "__minisHidden", state.box.minisHidden || {}, true);
this._proxyAssign("combineAs", "_combineAs", "__combineAs", state.box.combineAs || {}, true);
this._filters.forEach(it => it.setStateFromLoaded(state.filters, {isUserSavedState}));
}
_getSaveableState () {
const filterOut = {};
this._filters.forEach(it => Object.assign(filterOut, it.getSaveableState()));
return {
box: {
meta: {...this.__meta},
minisHidden: {...this.__minisHidden},
combineAs: {...this.__combineAs},
},
filters: filterOut,
};
}
async _pDoSaveState () {
await StorageUtil.pSetForPage(this._getNamespacedStorageKey(), this._getSaveableState());
}
trimState_ () {
this._filters.forEach(f => f.trimState_());
}
render () {
if (this._isRendered) {
// already rendered previously; simply update the filters
this._filters.map(f => f.update());
return;
}
this._isRendered = true;
if (this._$wrpFormTop || this._$wrpMiniPills) {
if (!this._$wrpMiniPills) {
this._$wrpMiniPills = $(`<div class="fltr__mini-view btn-group"></div>`).insertAfter(this._$wrpFormTop);
} else {
this._$wrpMiniPills.addClass("fltr__mini-view");
}
}
if (this._$btnReset) {
this._$btnReset
.title(TITLE_BTN_RESET)
.click((evt) => this.reset(evt.shiftKey));
}
if (this._$wrpFormTop || this._$btnToggleSummaryHidden) {
if (!this._$btnToggleSummaryHidden) {
this._$btnToggleSummaryHidden = $(`<button class="btn btn-default ${this._isCompact ? "p-2" : ""}" title="Toggle Filter Summary"><span class="glyphicon glyphicon-resize-small"></span></button>`)
.prependTo(this._$wrpFormTop);
} else if (!this._$btnToggleSummaryHidden.parent().length) {
this._$btnToggleSummaryHidden.prependTo(this._$wrpFormTop);
}
this._$btnToggleSummaryHidden
.click(() => {
this._meta.isSummaryHidden = !this._meta.isSummaryHidden;
this._doSaveStateThrottled();
});
const summaryHiddenHook = () => {
this._$btnToggleSummaryHidden.toggleClass("active", !!this._meta.isSummaryHidden);
this._$wrpMiniPills.toggleClass("ve-hidden", !!this._meta.isSummaryHidden);
};
this._addHook("meta", "isSummaryHidden", summaryHiddenHook);
summaryHiddenHook();
}
if (this._$wrpFormTop || this._$btnOpen) {
if (!this._$btnOpen) {
this._$btnOpen = $(`<button class="btn btn-default ${this._isCompact ? "px-2" : ""}">Filter</button>`)
.prependTo(this._$wrpFormTop);
} else if (!this._$btnOpen.parent().length) {
this._$btnOpen.prependTo(this._$wrpFormTop);
}
this._$btnOpen.click(() => this.show());
}
const sourceFilter = this._filters.find(it => it.header === SOURCE_HEADER);
if (sourceFilter) {
const selFnAlt = (val) => !SourceUtil.isNonstandardSource(val) && !PrereleaseUtil.hasSourceJson(val) && !BrewUtil2.hasSourceJson(val);
const hkSelFn = () => {
if (this._meta.isBrewDefaultHidden) sourceFilter.setTempFnSel(selFnAlt);
else sourceFilter.setTempFnSel(null);
sourceFilter.updateMiniPillClasses();
};
this._addHook("meta", "isBrewDefaultHidden", hkSelFn);
hkSelFn();
}
if (this._$wrpMiniPills) this._filters.map((f, i) => f.$renderMinis({filterBox: this, isFirst: i === 0, $wrpMini: this._$wrpMiniPills}));
}
async _render_pRenderModal () {
this._isModalRendered = true;
this._modalMeta = await UiUtil.pGetShowModal({
isHeight100: true,
isWidth100: true,
isUncappedHeight: true,
isIndestructible: true,
isClosed: true,
isEmpty: true,
title: "Filter", // Not shown due toe `isEmpty`, but useful for external overrides
cbClose: (isDataEntered) => this._pHandleHide(!isDataEntered),
});
const $children = this._filters.map((f, i) => f.$render({filterBox: this, isFirst: i === 0, $wrpMini: this._$wrpMiniPills}));
this._metaIptSearch = ComponentUiUtil.$getIptStr(
this._compSearch, "search",
{decorationRight: "clear", asMeta: true, html: `<input class="form-control input-xs" placeholder="Search...">`},
);
this._compSearch._addHookBase("search", () => {
const searchTerm = this._compSearch._state.search.toLowerCase();
this._filters.forEach(f => f.handleSearch(searchTerm));
});
const $btnShowAllFilters = $(`<button class="btn btn-xs btn-default">Show All</button>`)
.click(() => this.showAllFilters());
const $btnHideAllFilters = $(`<button class="btn btn-xs btn-default">Hide All</button>`)
.click(() => this.hideAllFilters());
const $btnReset = $(`<button class="btn btn-xs btn-default mr-3" title="${TITLE_BTN_RESET}">Reset</button>`)
.click(evt => this.reset(evt.shiftKey));
const $btnSettings = $(`<button class="btn btn-xs btn-default mr-3"><span class="glyphicon glyphicon-cog"></span></button>`)
.click(() => this._pOpenSettingsModal());
const $btnSaveAlt = $(`<button class="btn btn-xs btn-primary" title="Save"><span class="glyphicon glyphicon-ok"></span></button>`)
.click(() => this._modalMeta.doClose(true));
const $wrpBtnCombineFilters = $(`<div class="btn-group mr-3"></div>`);
const $btnCombineFilterSettings = $(`<button class="btn btn-xs btn-default"><span class="glyphicon glyphicon-cog"></span></button>`)
.click(() => this._pOpenCombineAsModal());
const btnCombineFiltersAs = e_({
tag: "button",
clazz: `btn btn-xs btn-default`,
click: () => this._meta.modeCombineFilters = FilterBox._COMBINE_MODES.getNext(this._meta.modeCombineFilters),
title: `"AND" requires every filter to match. "OR" requires any filter to match. "Custom" allows you to specify a combination (every "AND" filter must match; only one "OR" filter must match) .`,
}).appendTo($wrpBtnCombineFilters[0]);
const hook = () => {
btnCombineFiltersAs.innerText = this._meta.modeCombineFilters === "custom" ? this._meta.modeCombineFilters.uppercaseFirst() : this._meta.modeCombineFilters.toUpperCase();
if (this._meta.modeCombineFilters === "custom") $wrpBtnCombineFilters.append($btnCombineFilterSettings);
else $btnCombineFilterSettings.detach();
this._doSaveStateThrottled();
};
this._addHook("meta", "modeCombineFilters", hook);
hook();
const $btnSave = $(`<button class="btn btn-primary fltr__btn-close mr-2">Save</button>`)
.click(() => this._modalMeta.doClose(true));
const $btnCancel = $(`<button class="btn btn-default fltr__btn-close">Cancel</button>`)
.click(() => this._modalMeta.doClose(false));
$$(this._modalMeta.$modal)`<div class="split mb-2 mt-2 ve-flex-v-center mobile__ve-flex-col">
<div class="ve-flex-v-baseline mobile__ve-flex-col">
<h4 class="m-0 mr-2 mobile__mb-2">Filters</h4>
${this._metaIptSearch.$wrp.addClass("mobile__mb-2")}
</div>
<div class="ve-flex-v-center mobile__ve-flex-col">
<div class="ve-flex-v-center mobile__m-1">
<div class="mr-2">Combine as</div>
${$wrpBtnCombineFilters}
</div>
<div class="ve-flex-v-center mobile__m-1">
<div class="btn-group mr-2 ve-flex-h-center">
${$btnShowAllFilters}
${$btnHideAllFilters}
</div>
${$btnReset}
${$btnSettings}
${$btnSaveAlt}
</div>
</div>
</div>
<hr class="w-100 m-0 mb-2">
<hr class="mt-1 mb-1">
<div class="ui-modal__scroller smooth-scroll px-1">
${$children}
</div>
<hr class="my-1 w-100">
<div class="w-100 ve-flex-vh-center my-1">${$btnSave}${$btnCancel}</div>`;
}
async _pOpenSettingsModal () {
const {$modalInner} = await UiUtil.pGetShowModal({title: "Settings"});
UiUtil.$getAddModalRowCb($modalInner, "Deselect Homebrew Sources by Default", this._meta, "isBrewDefaultHidden");
UiUtil.addModalSep($modalInner);
UiUtil.$getAddModalRowHeader($modalInner, "Hide summary for filter...", {helpText: "The summary is the small red and blue button panel which appear below the search bar."});
this._filters.forEach(f => UiUtil.$getAddModalRowCb($modalInner, f.header, this._minisHidden, f.header));
UiUtil.addModalSep($modalInner);
const $rowResetAlwaysSave = UiUtil.$getAddModalRow($modalInner, "div").addClass("pr-2");
$rowResetAlwaysSave.append(`<span>Always Save on Close</span>`);
$(`<button class="btn btn-xs btn-default">Reset</button>`)
.appendTo($rowResetAlwaysSave)
.click(async () => {
await StorageUtil.pRemove(FilterBox._STORAGE_KEY_ALWAYS_SAVE_UNCHANGED);
JqueryUtil.doToast("Saved!");
});
}
async _pOpenCombineAsModal () {
const {$modalInner} = await UiUtil.pGetShowModal({title: "Filter Combination Logic"});
const $btnReset = $(`<button class="btn btn-xs btn-default">Reset</button>`)
.click(() => {
Object.keys(this._combineAs).forEach(k => this._combineAs[k] = "and");
$sels.forEach($sel => $sel.val("0"));
});
UiUtil.$getAddModalRowHeader($modalInner, "Combine filters as...", {$eleRhs: $btnReset});
const $sels = this._filters.map(f => UiUtil.$getAddModalRowSel($modalInner, f.header, this._combineAs, f.header, ["and", "or"], {fnDisplay: (it) => it.toUpperCase()}));
}
getValues ({nxtStateOuter = null} = {}) {
const outObj = {};
this._filters.forEach(f => Object.assign(outObj, f.getValues({nxtState: nxtStateOuter?.filters})));
return outObj;
}
addEventListener (type, listener) {
(this._$wrpFormTop ? this._$wrpFormTop[0] : this._$btnOpen[0]).addEventListener(type, listener);
}
_mutNextState_reset_meta ({tgt}) {
Object.assign(tgt, this._getDefaultMeta());
}
_mutNextState_minisHidden ({tgt}) {
Object.assign(tgt, this._getDefaultMinisHidden(tgt));
}
_mutNextState_combineAs ({tgt}) {
Object.assign(tgt, this._getDefaultCombineAs(tgt));
}
_reset_meta () {
const nxtBoxState = this._getNextBoxState_base();
this._mutNextState_reset_meta({tgt: nxtBoxState.meta});
this._setBoxStateFromNextBoxState(nxtBoxState);
}
_reset_minisHidden () {
const nxtBoxState = this._getNextBoxState_base();
this._mutNextState_minisHidden({tgt: nxtBoxState.minisHidden});
this._setBoxStateFromNextBoxState(nxtBoxState);
}
_reset_combineAs () {
const nxtBoxState = this._getNextBoxState_base();
this._mutNextState_combineAs({tgt: nxtBoxState.combineAs});
this._setBoxStateFromNextBoxState(nxtBoxState);
}
reset (isResetAll) {
this._filters.forEach(f => f.reset({isResetAll}));
if (isResetAll) {
this._reset_meta();
this._reset_minisHidden();
this._reset_combineAs();
}
this.render();
this.fireChangeEvent();
}
async show () {
if (!this._isModalRendered) await this._render_pRenderModal();
this._cachedState = this._getSaveableState();
this._modalMeta.doOpen();
if (this._metaIptSearch?.$ipt) this._metaIptSearch.$ipt.focus();
}
async _pHandleHide (isCancel = false) {
if (this._cachedState && isCancel) {
const curState = this._getSaveableState();
const hasChanges = !CollectionUtil.deepEquals(curState, this._cachedState);
if (hasChanges) {
const isSave = await InputUiUtil.pGetUserBoolean({
title: "Unsaved Changes",
textYesRemember: "Always Save",
textYes: "Save",
textNo: "Discard",
storageKey: FilterBox._STORAGE_KEY_ALWAYS_SAVE_UNCHANGED,
isGlobal: true,
});
if (isSave) {
this._cachedState = null;
this.fireChangeEvent();
return;
} else this._setStateFromLoaded(this._cachedState, {isUserSavedState: true});
}
} else {
this.fireChangeEvent();
}
this._cachedState = null;
}
showAllFilters () {
this._filters.forEach(f => f.show());
}
hideAllFilters () {
this._filters.forEach(f => f.hide());
}
unpackSubHashes (subHashes, {force = false} = {}) {
// TODO(unpack) refactor
const unpacked = {};
subHashes.forEach(s => {
const unpackedPart = UrlUtil.unpackSubHash(s, true);
if (Object.keys(unpackedPart).length > 1) throw new Error(`Multiple keys in subhash!`);
const k = Object.keys(unpackedPart)[0];
unpackedPart[k] = {clean: unpackedPart[k], raw: s};
Object.assign(unpacked, unpackedPart);
});
const urlHeaderToFilter = {};
this._filters.forEach(f => {
const childFilters = f.getChildFilters();
if (childFilters.length) childFilters.forEach(f => urlHeaderToFilter[f.header.toLowerCase()] = f);
urlHeaderToFilter[f.header.toLowerCase()] = f;
});
const urlHeadersUpdated = new Set();
const subHashesConsumed = new Set();
let filterInitialSearch;
const filterBoxState = {};
const statePerFilter = {};
const prefixLen = this.getNamespacedHashKey().length;
Object.entries(unpacked)
.forEach(([hashKey, data]) => {
const rawPrefix = hashKey.substring(0, prefixLen);
const prefix = rawPrefix.substring(0, SUB_HASH_PREFIX_LENGTH);
const urlHeader = hashKey.substring(prefixLen);
if (FilterRegistry.SUB_HASH_PREFIXES.has(prefix) && urlHeaderToFilter[urlHeader]) {
(statePerFilter[urlHeader] = statePerFilter[urlHeader] || {})[prefix] = data.clean;
urlHeadersUpdated.add(urlHeader);
subHashesConsumed.add(data.raw);
return;
}
if (Object.values(FilterBox._SUB_HASH_PREFIXES).includes(prefix)) {
// special case for the search """state"""
if (prefix === VeCt.FILTER_BOX_SUB_HASH_SEARCH_PREFIX) filterInitialSearch = data.clean[0];
else filterBoxState[prefix] = data.clean;
subHashesConsumed.add(data.raw);
return;
}
if (FilterRegistry.SUB_HASH_PREFIXES.has(prefix)) throw new Error(`Could not find filter with header ${urlHeader} for subhash ${data.raw}`);
});
if (!subHashesConsumed.size && !force) return null;
return {
urlHeaderToFilter,
filterBoxState,
statePerFilter,
urlHeadersUpdated,
unpacked,
subHashesConsumed,
filterInitialSearch,
};
}
setFromSubHashes (subHashes, {force = false, $iptSearch = null} = {}) {
const unpackedSubhashes = this.unpackSubHashes(subHashes, {force});
if (unpackedSubhashes == null) return subHashes;
const {
unpacked,
subHashesConsumed,
filterInitialSearch,
} = unpackedSubhashes;
// region Update filter state
const {box: nxtStateBox, filters: nxtStatesFilters} = this.getNextStateFromSubHashes({unpackedSubhashes});
this._setBoxStateFromNextBoxState(nxtStateBox);
this._filters
.flatMap(f => [
f,
...f.getChildFilters(),
])
.filter(filter => nxtStatesFilters[filter.header])
.forEach(filter => filter.setStateFromNextState(nxtStatesFilters));
// endregion
// region Update search input value
if (filterInitialSearch && ($iptSearch || this._$iptSearch)) ($iptSearch || this._$iptSearch).val(filterInitialSearch).change().keydown().keyup().trigger("instantKeyup");
// endregion
// region Re-assemble and return remaining subhashes
const [link] = Hist.getHashParts();
const outSub = [];
Object.values(unpacked)
.filter(v => !subHashesConsumed.has(v.raw))
.forEach(v => outSub.push(v.raw));
Hist.setSuppressHistory(true);
Hist.replaceHistoryHash(`${link}${outSub.length ? `${HASH_PART_SEP}${outSub.join(HASH_PART_SEP)}` : ""}`);
this.fireChangeEvent();
Hist.hashChange({isBlankFilterLoad: true});
return outSub;
// endregion
}
getNextStateFromSubHashes ({unpackedSubhashes}) {
const {
urlHeaderToFilter,
filterBoxState,
statePerFilter,
urlHeadersUpdated,
} = unpackedSubhashes;
const nxtStateBox = this._getNextBoxStateFromSubHashes(urlHeaderToFilter, filterBoxState);
const nxtStateFilters = {};
Object.entries(statePerFilter)
.forEach(([urlHeader, state]) => {
const filter = urlHeaderToFilter[urlHeader];
Object.assign(nxtStateFilters, filter.getNextStateFromSubhashState(state));
});
// reset any other state/meta state/etc
Object.keys(urlHeaderToFilter)
.filter(k => !urlHeadersUpdated.has(k))
.forEach(k => {
const filter = urlHeaderToFilter[k];
Object.assign(nxtStateFilters, filter.getNextStateFromSubhashState(null));
});
return {box: nxtStateBox, filters: nxtStateFilters};
}
_getNextBoxState_base () {
return {
meta: MiscUtil.copyFast(this.__meta),
minisHidden: MiscUtil.copyFast(this.__minisHidden),
combineAs: MiscUtil.copyFast(this.__combineAs),
};
}
_getNextBoxStateFromSubHashes (urlHeaderToFilter, filterBoxState) {
const nxtBoxState = this._getNextBoxState_base();
let hasMeta = false;
let hasMinisHidden = false;
let hasCombineAs = false;
Object.entries(filterBoxState).forEach(([k, vals]) => {
const mappedK = this.getNamespacedHashKey(Parser._parse_bToA(FilterBox._SUB_HASH_PREFIXES, k));
switch (mappedK) {
case "meta": {
hasMeta = true;
const data = vals.map(v => UrlUtil.mini.decompress(v));
Object.keys(this._getDefaultMeta()).forEach((k, i) => nxtBoxState.meta[k] = data[i]);
break;
}
case "minisHidden": {
hasMinisHidden = true;
Object.keys(nxtBoxState.minisHidden).forEach(k => nxtBoxState.minisHidden[k] = false);
vals.forEach(v => {
const [urlHeader, isHidden] = v.split("=");
const filter = urlHeaderToFilter[urlHeader];
if (!filter) throw new Error(`Could not find filter with name "${urlHeader}"`);
nxtBoxState.minisHidden[filter.header] = !!Number(isHidden);
});
break;
}
case "combineAs": {
hasCombineAs = true;
Object.keys(nxtBoxState.combineAs).forEach(k => nxtBoxState.combineAs[k] = "and");
vals.forEach(v => {
const [urlHeader, ixCombineMode] = v.split("=");
const filter = urlHeaderToFilter[urlHeader];
if (!filter) throw new Error(`Could not find filter with name "${urlHeader}"`);
nxtBoxState.combineAs[filter.header] = FilterBox._COMBINE_MODES[ixCombineMode] || FilterBox._COMBINE_MODES[0];
});
break;
}
}
});
if (!hasMeta) this._mutNextState_reset_meta({tgt: nxtBoxState.meta});
if (!hasMinisHidden) this._mutNextState_minisHidden({tgt: nxtBoxState.minisHidden});
if (!hasCombineAs) this._mutNextState_combineAs({tgt: nxtBoxState.combineAs});
return nxtBoxState;
}
_setBoxStateFromNextBoxState (nxtBoxState) {
this._proxyAssignSimple("meta", nxtBoxState.meta, true);
this._proxyAssignSimple("minisHidden", nxtBoxState.minisHidden, true);
this._proxyAssignSimple("combineAs", nxtBoxState.combineAs, true);
}
/**
* @param [opts] Options object.
* @param [opts.isAddSearchTerm] If the active search should be added to the subhashes.
*/
getSubHashes (opts) {
opts = opts || {};
const out = [];
const boxSubHashes = this.getBoxSubHashes();
if (boxSubHashes) out.push(boxSubHashes);
out.push(...this._filters.map(f => f.getSubHashes()).filter(Boolean));
if (opts.isAddSearchTerm && this._$iptSearch) {
const searchTerm = UrlUtil.encodeForHash(this._$iptSearch.val().trim());
if (searchTerm) out.push(UrlUtil.packSubHash(this._getSubhashPrefix("search"), [searchTerm]));
}
return out.flat();
}
getBoxSubHashes () {
const out = [];
const defaultMeta = this._getDefaultMeta();
// serialize base meta in a set order
const anyNotDefault = Object.keys(defaultMeta).find(k => this._meta[k] !== defaultMeta[k]);
if (anyNotDefault) {
const serMeta = Object.keys(defaultMeta).map(k => UrlUtil.mini.compress(this._meta[k] === undefined ? defaultMeta[k] : this._meta[k]));
out.push(UrlUtil.packSubHash(this._getSubhashPrefix("meta"), serMeta));
}
// serialize minisHidden as `key=value` pairs
const setMinisHidden = Object.entries(this._minisHidden).filter(([k, v]) => !!v).map(([k]) => `${k.toUrlified()}=1`);
if (setMinisHidden.length) {
out.push(UrlUtil.packSubHash(this._getSubhashPrefix("minisHidden"), setMinisHidden));
}
// serialize combineAs as `key=value` pairs
const setCombineAs = Object.entries(this._combineAs).filter(([k, v]) => v !== FilterBox._COMBINE_MODES[0]).map(([k, v]) => `${k.toUrlified()}=${FilterBox._COMBINE_MODES.indexOf(v)}`);
if (setCombineAs.length) {
out.push(UrlUtil.packSubHash(this._getSubhashPrefix("combineAs"), setCombineAs));
}
return out.length ? out : null;
}
getFilterTag ({isAddSearchTerm = false} = {}) {
const parts = this._filters.map(f => f.getFilterTagPart()).filter(Boolean);
if (isAddSearchTerm && this._$iptSearch) {
const term = this._$iptSearch.val().trim();
if (term) parts.push(`search=${term}`);
}
return `{@filter |${UrlUtil.getCurrentPage().replace(/\.html$/, "")}|${parts.join("|")}}`;
}
getDisplayState ({nxtStateOuter = null} = {}) {
return this._filters
.map(filter => filter.getDisplayStatePart({nxtState: nxtStateOuter?.filters}))
.filter(Boolean)
.join("; ");
}
setFromValues (values) {
this._filters.forEach(it => it.setFromValues(values));
this.fireChangeEvent();
}
toDisplay (boxState, ...entryVals) {
return this._toDisplay(boxState, this._filters, entryVals);
}
/** `filterToValueTuples` should be an array of `{filter: <Filter>, value: <Any>}` objects */
toDisplayByFilters (boxState, ...filterToValueTuples) {
return this._toDisplay(
boxState,
filterToValueTuples.map(it => it.filter),
filterToValueTuples.map(it => it.value),
);
}
_toDisplay (boxState, filters, entryVals) {
switch (this._meta.modeCombineFilters) {
case "and": return this._toDisplay_isAndDisplay(boxState, filters, entryVals);
case "or": return this._toDisplay_isOrDisplay(boxState, filters, entryVals);
case "custom": {
if (entryVals.length !== filters.length) throw new Error(`Number of filters and number of values did not match!`);
const andFilters = [];
const andValues = [];
const orFilters = [];
const orValues = [];
for (let i = 0; i < filters.length; ++i) {
const f = filters[i];
if (!this._combineAs[f.header] || this._combineAs[f.header] === "and") { // default to "and" if undefined
andFilters.push(f);
andValues.push(entryVals[i]);
} else {
orFilters.push(f);
orValues.push(entryVals[i]);
}
}
return this._toDisplay_isAndDisplay(boxState, andFilters, andValues) && this._toDisplay_isOrDisplay(boxState, orFilters, orValues);
}
default: throw new Error(`Unhandled combining mode "${this._meta.modeCombineFilters}"`);
}
}
_toDisplay_isAndDisplay (boxState, filters, vals) {
return filters
.map((f, i) => f.toDisplay(boxState, vals[i]))
.every(it => it);
}
_toDisplay_isOrDisplay (boxState, filters, vals) {
const res = filters.map((f, i) => {
// filter out "ignored" filter (i.e. all white)
if (!f.isActive(boxState)) return null;
return f.toDisplay(boxState, vals[i]);
}).filter(it => it != null);
return res.length === 0 || res.find(it => it);
}
_getSubhashPrefix (prop) {
if (FilterBox._SUB_HASH_PREFIXES[prop]) return this.getNamespacedHashKey(FilterBox._SUB_HASH_PREFIXES[prop]);
throw new Error(`Unknown property "${prop}"`);
}
_getDefaultMeta () {
const out = MiscUtil.copy(FilterBox._DEFAULT_META);
if (this._isCompact) out.isSummaryHidden = true;
return out;
}
_getDefaultMinisHidden (minisHidden) {
if (!minisHidden) throw new Error(`Missing "minisHidden" argument!`);
return Object.keys(minisHidden)
.mergeMap(k => ({[k]: false}));
}
_getDefaultCombineAs (combineAs) {
if (!combineAs) throw new Error(`Missing "combineAs" argument!`);
return Object.keys(combineAs)
.mergeMap(k => ({[k]: "and"}));
}
}
FilterBox._PILL_STATES = ["ignore", "yes", "no"];
FilterBox._COMBINE_MODES = ["and", "or", "custom"];
FilterBox._STORAGE_KEY = "filterBoxState";
FilterBox._DEFAULT_META = {
modeCombineFilters: "and",
isSummaryHidden: false,
isBrewDefaultHidden: false,
};
FilterBox._STORAGE_KEY_ALWAYS_SAVE_UNCHANGED = "filterAlwaysSaveUnchanged";
// These are assumed to be the same length (4 characters)
FilterBox._SUB_HASH_BOX_META_PREFIX = "fbmt";
FilterBox._SUB_HASH_BOX_MINIS_HIDDEN_PREFIX = "fbmh";
FilterBox._SUB_HASH_BOX_COMBINE_AS_PREFIX = "fbca";
FilterBox._SUB_HASH_PREFIXES = {
meta: FilterBox._SUB_HASH_BOX_META_PREFIX,
minisHidden: FilterBox._SUB_HASH_BOX_MINIS_HIDDEN_PREFIX,
combineAs: FilterBox._SUB_HASH_BOX_COMBINE_AS_PREFIX,
search: VeCt.FILTER_BOX_SUB_HASH_SEARCH_PREFIX,
};
FilterRegistry.registerSubhashes(Object.values(FilterBox._SUB_HASH_PREFIXES));