-
Notifications
You must be signed in to change notification settings - Fork 0
/
filters.ts
141 lines (129 loc) · 4.33 KB
/
filters.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import selectDefaults from "./filters.select";
import bs5Defaults from "./filters.bs5";
const defaults: { [key: string]: AttrSpecs } = {
select: selectDefaults,
bs5: bs5Defaults,
};
export function render(opt: RenderOptions): void | Promise<any> {
// If no .data, fetch from .url, else fail
if (!opt.data)
if (opt.url) {
const url: RequestInfo | URL = opt.url;
return new Promise((resolve) => {
fetch(url)
.then((r) => r.json())
.then((data) => {
render({ ...opt, data });
resolve(data);
});
});
// TODO: test error handling
} else throw new Error(`filters: missing options {url , data}`);
const root =
opt.container instanceof Element
? opt.container
: document.querySelector(opt.container);
// TODO: test error handling
if (!root) throw new Error(`filters: missing {container: ${opt.container}}`);
const type = opt.type || "select";
// Convert all field/value values into field name -> { spec }
const fieldSpec = getSpecs(
Object.keys(opt.data),
defaults[type].fields,
opt.fields,
opt.field
);
const valueSpec = getSpecs(
Object.keys(opt.data),
defaults[type].values,
opt.values,
opt.value
);
for (const [name, values] of Object.entries(opt.data)) {
let render: Function,
field: { [key: string]: Function },
value: { [key: string]: Function };
({ render, ...field } = fieldSpec[name]);
const fieldData: FieldSpec = { name: name, values: values };
for (const [attr, func] of Object.entries(field))
fieldData[attr] = func(fieldData);
let el = root.querySelector(fieldData.selector as string);
if (!el) {
root.insertAdjacentHTML("beforeend", render(fieldData));
el = root.querySelector(fieldData.selector as string);
}
// TODO: test error handling
if (!el)
throw new Error(
`filters: field ${name} missing {selector: ${fieldData.selector}}`
);
({ render, ...value } = valueSpec[name]);
// Insert default value if required and missing
const rows =
typeof fieldData.default == "undefined" ||
values.includes(fieldData.default)
? values
: [fieldData.default, ...values];
el.innerHTML = rows
.map((row) => {
const valueData = typeof row == "object" ? row : { value: row };
for (const [attr, func] of Object.entries(value))
valueData[attr] = func(valueData);
return render(valueData, fieldData);
})
.join("");
}
}
const functor = (v: any) => (typeof v === "function" ? v : () => v);
// TODO: JSDoc this like crazy. It's complicated.
// maps: .attr = value | .attr.name = value
// map: .name.attr = value
// attrMap = .name.attr = value
function getSpecs(
names: string[],
defaults: AttrSpec,
maps: AttrSpec,
map: AttrSpecs
) {
const specs: AttrSpecs = {};
for (const [attr, value] of Object.entries(Object.assign({}, defaults, maps)))
if (typeof value == "object")
for (const [name, val] of Object.entries(value))
(specs[name] ??= {})[attr] = functor(val);
else for (const name of names) (specs[name] ??= {})[attr] = functor(value);
for (const [name, attrs] of Object.entries(map || {}))
for (const [attr, value] of Object.entries(attrs))
(specs[name] ??= {})[attr] = functor(value);
return specs;
}
export function attrMap(attrs: AttrSpec): string {
/** Convert attributes object to string of HTML attributes */
const html: string[] = [];
for (let [key, value] of Object.entries(attrs))
if (typeof value == "boolean") {
if (value) html.push(key);
} else {
html.push(`${key}="${value.toString().replace(/"/g, """)}"`);
}
return html.join(" ");
}
export type AttrSpec = { [key: string]: any };
type AttrSpecs = { [key: string]: AttrSpec };
type FieldSpec = {
render?: Function;
selector?: string | ((...args: any[]) => string);
default?: string | Function;
multiple?: string | Function;
value?: string | Function;
} & AttrSpec;
// TODO: Rename RenderOptions to FilterOptions
interface RenderOptions {
type: "select" | "bs5" | "bootstrap-select" | "select2" | "selectize";
container: string | Element;
url?: RequestInfo | URL;
data?: { [key: string]: (object | string)[] };
fields: FieldSpec;
field: { [key: string]: FieldSpec };
values: AttrSpec;
value: AttrSpecs;
}