@livenetworks/ashlar 2.0.6 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +641 -641
- package/components/COMPONENTS.md +1116 -1116
- package/components/index.js +81 -81
- package/components/ln-accordion/ln-accordion-dev.scss +8 -8
- package/components/ln-accordion/ln-accordion.js +1 -1
- package/components/ln-accordion/ln-accordion.schema.json +30 -30
- package/components/ln-accordion/src/ln-accordion.js +1 -0
- package/components/ln-ajax/ln-ajax-dev.scss +8 -8
- package/components/ln-ajax/ln-ajax.js +1 -1
- package/components/ln-ajax/ln-ajax.schema.json +30 -30
- package/components/ln-api-connector/ln-api-connector.js +1 -1
- package/components/ln-api-connector/ln-api-connector.schema.json +96 -96
- package/components/ln-api-connector/src/ln-api-connector.js +1 -0
- package/components/ln-api-queue/ln-api-queue.js +1 -1
- package/components/ln-api-queue/ln-api-queue.schema.json +34 -34
- package/components/ln-api-queue/src/ln-api-queue.js +3 -13
- package/components/ln-autoresize/ln-autoresize-dev.scss +8 -8
- package/components/ln-autoresize/ln-autoresize.js +1 -1
- package/components/ln-autoresize/ln-autoresize.schema.json +22 -22
- package/components/ln-autosave/ln-autosave-dev.scss +13 -13
- package/components/ln-autosave/ln-autosave.js +1 -1
- package/components/ln-autosave/ln-autosave.schema.json +48 -48
- package/components/ln-autosave/src/autosave-model.js +34 -34
- package/components/ln-chart/ln-chart.js +1 -1
- package/components/ln-chart/ln-chart.schema.json +146 -146
- package/components/ln-chart/src/chart-model.js +154 -154
- package/components/ln-circular-progress/ln-circular-progress.js +1 -1
- package/components/ln-circular-progress/ln-circular-progress.schema.json +42 -42
- package/components/ln-confirm/ln-confirm-dev.scss +24 -24
- package/components/ln-confirm/ln-confirm.js +1 -1
- package/components/ln-confirm/ln-confirm.schema.json +66 -66
- package/components/ln-confirm/src/ln-confirm.js +178 -178
- package/components/ln-core/crypto.js +286 -265
- package/components/ln-core/dom.js +115 -115
- package/components/ln-core/form.js +136 -136
- package/components/ln-core/helpers.js +33 -33
- package/components/ln-core/http.js +48 -48
- package/components/ln-core/index.js +1 -1
- package/components/ln-core/ln-core.schema.json +109 -109
- package/components/ln-core/locale.js +55 -55
- package/components/ln-core/matching.js +78 -78
- package/components/ln-core/number.js +125 -125
- package/components/ln-core/template.js +206 -206
- package/components/ln-couchdb-connector/README.md +4 -0
- package/components/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/components/ln-couchdb-connector/ln-couchdb-connector.schema.json +50 -50
- package/components/ln-couchdb-connector/src/ln-couchdb-connector.js +19 -1
- package/components/ln-data-coordinator/README.md +9 -38
- package/components/ln-data-coordinator/ln-data-coordinator-dev.scss +8 -8
- package/components/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/components/ln-data-coordinator/ln-data-coordinator.schema.json +16 -16
- package/components/ln-data-coordinator/src/data-read-policy.js +0 -7
- package/components/ln-data-coordinator/src/ln-data-coordinator.js +428 -1299
- package/components/ln-data-store/README.md +3 -2
- package/components/ln-data-store/ln-data-store-dev.scss +13 -13
- package/components/ln-data-store/ln-data-store.js +1 -1
- package/components/ln-data-store/ln-data-store.schema.json +84 -84
- package/components/ln-data-store/src/data-store-model.js +184 -155
- package/components/ln-data-store/src/ln-data-store.js +58 -11
- package/components/ln-date/README.md +4 -0
- package/components/ln-date/ln-date.js +1 -1
- package/components/ln-date/ln-date.schema.json +92 -92
- package/components/ln-date/src/date-model.js +186 -186
- package/components/ln-date/src/ln-date.js +3 -3
- package/components/ln-debug/ln-debug.js +1 -1
- package/components/ln-debug/ln-debug.schema.json +28 -28
- package/components/ln-debug/src/console-sink.js +19 -19
- package/components/ln-debug/src/gate.js +95 -95
- package/components/ln-debug/src/generated-attributes.js +2 -2
- package/components/ln-dropdown/ln-dropdown-dev.scss +13 -13
- package/components/ln-dropdown/ln-dropdown.js +1 -1
- package/components/ln-dropdown/ln-dropdown.schema.json +90 -90
- package/components/ln-editor/ln-editor.js +1 -1
- package/components/ln-editor/ln-editor.schema.json +58 -58
- package/components/ln-external-links/ln-external-links.js +1 -1
- package/components/ln-external-links/ln-external-links.schema.json +18 -18
- package/components/ln-fill/ln-fill.js +1 -1
- package/components/ln-fill/ln-fill.schema.json +23 -23
- package/components/ln-filter/ln-filter.js +1 -1
- package/components/ln-filter/ln-filter.schema.json +153 -153
- package/components/ln-filter/src/filter-model.js +185 -185
- package/components/ln-form/ln-form-dev.scss +8 -8
- package/components/ln-form/ln-form.js +1 -1
- package/components/ln-form/ln-form.schema.json +45 -45
- package/components/ln-http/ln-http.js +1 -1
- package/components/ln-http/ln-http.schema.json +6 -6
- package/components/ln-icon/ln-icon.schema.json +6 -6
- package/components/ln-include/ln-include.js +1 -1
- package/components/ln-include/ln-include.schema.json +23 -23
- package/components/ln-key/ln-key-dev.scss +18 -18
- package/components/ln-key/ln-key.js +1 -1
- package/components/ln-key/ln-key.schema.json +56 -56
- package/components/ln-key/src/ln-key.js +10 -10
- package/components/ln-link/ln-link-dev.scss +8 -8
- package/components/ln-link/ln-link.js +1 -1
- package/components/ln-link/ln-link.schema.json +22 -22
- package/components/ln-list/README.md +152 -152
- package/components/ln-list/ln-list-dev.scss +8 -8
- package/components/ln-list/ln-list.js +1 -1
- package/components/ln-list/ln-list.schema.json +166 -166
- package/components/ln-list/src/ln-list.js +4 -4
- package/components/ln-modal/README.md +4 -6
- package/components/ln-modal/ln-modal.js +1 -1
- package/components/ln-modal/ln-modal.schema.json +65 -65
- package/components/ln-modal/src/ln-modal.js +28 -8
- package/components/ln-nav/ln-nav.js +1 -1
- package/components/ln-nav/ln-nav.schema.json +25 -25
- package/components/ln-number/ln-number.js +1 -1
- package/components/ln-number/ln-number.schema.json +63 -63
- package/components/ln-number/src/number-model.js +31 -31
- package/components/ln-obfuscator/README.md +60 -60
- package/components/ln-obfuscator/ln-obfuscator.js +1 -1
- package/components/ln-obfuscator/ln-obfuscator.schema.json +49 -49
- package/components/ln-obfuscator/src/ln-obfuscator.js +159 -159
- package/components/ln-obfuscator/src/obfuscator-model.js +250 -250
- package/components/ln-options/ln-options.js +1 -1
- package/components/ln-options/ln-options.schema.json +41 -41
- package/components/ln-options/src/ln-options.js +1 -0
- package/components/ln-persist/README.md +151 -151
- package/components/ln-persist/ln-persist.js +1 -1
- package/components/ln-persist/ln-persist.schema.json +28 -28
- package/components/ln-persist/src/ln-persist.js +83 -83
- package/components/ln-picklist/README.md +230 -230
- package/components/ln-picklist/ln-picklist-dev.scss +22 -22
- package/components/ln-picklist/ln-picklist.js +1 -1
- package/components/ln-picklist/ln-picklist.schema.json +49 -49
- package/components/ln-picklist/src/ln-picklist.js +228 -228
- package/components/ln-popover/README.md +4 -0
- package/components/ln-popover/ln-popover-dev.scss +13 -13
- package/components/ln-popover/ln-popover.js +1 -1
- package/components/ln-popover/ln-popover.schema.json +82 -82
- package/components/ln-popover/src/ln-popover.js +9 -0
- package/components/ln-progress/ln-progress-dev.scss +8 -8
- package/components/ln-progress/ln-progress.js +1 -1
- package/components/ln-progress/ln-progress.schema.json +40 -40
- package/components/ln-router/ln-router.js +1 -1
- package/components/ln-router/ln-router.schema.json +76 -76
- package/components/ln-scroll/README.md +45 -45
- package/components/ln-scroll/ln-scroll.js +1 -1
- package/components/ln-scroll/ln-scroll.schema.json +85 -85
- package/components/ln-scroll/src/ln-scroll.js +150 -150
- package/components/ln-search/README.md +188 -188
- package/components/ln-search/ln-search.js +1 -1
- package/components/ln-search/ln-search.schema.json +103 -103
- package/components/ln-slug/ln-slug.js +1 -1
- package/components/ln-slug/ln-slug.schema.json +23 -23
- package/components/ln-sort/ln-sort.js +1 -1
- package/components/ln-sort/ln-sort.schema.json +104 -104
- package/components/ln-sort/src/ln-sort.js +321 -321
- package/components/ln-sort/src/sort-model.js +117 -117
- package/components/ln-sortable/ln-sortable-dev.scss +13 -13
- package/components/ln-sortable/ln-sortable.js +1 -1
- package/components/ln-sortable/ln-sortable.schema.json +36 -36
- package/components/ln-stat/ln-stat-dev.scss +8 -8
- package/components/ln-stat/ln-stat.js +1 -1
- package/components/ln-stat/ln-stat.schema.json +32 -32
- package/components/ln-stat/src/stat-model.js +28 -28
- package/components/ln-table/ln-table.js +1 -1
- package/components/ln-table/ln-table.schema.json +211 -211
- package/components/ln-table/src/ln-table.js +2 -0
- package/components/ln-table/src/table-model.js +98 -98
- package/components/ln-table-coordinator/ln-table-coordinator.js +1 -1
- package/components/ln-table-coordinator/ln-table-coordinator.schema.json +93 -93
- package/components/ln-table-coordinator/src/ln-table-coordinator.js +3 -3
- package/components/ln-tabs/README.md +2 -2
- package/components/ln-tabs/ln-tabs.js +1 -1
- package/components/ln-tabs/ln-tabs.schema.json +75 -75
- package/components/ln-tabs/src/ln-tabs.js +208 -207
- package/components/ln-tabs/src/tabs-model.js +82 -82
- package/components/ln-time/ln-time-dev.scss +13 -13
- package/components/ln-time/ln-time.js +1 -1
- package/components/ln-time/ln-time.schema.json +38 -38
- package/components/ln-toast/ln-toast.js +1 -1
- package/components/ln-toast/ln-toast.schema.json +65 -65
- package/components/ln-toggle/ln-toggle.js +1 -1
- package/components/ln-toggle/ln-toggle.schema.json +52 -52
- package/components/ln-tooltip/ln-tooltip.js +1 -1
- package/components/ln-tooltip/ln-tooltip.schema.json +86 -86
- package/components/ln-translations/ln-translations.js +1 -1
- package/components/ln-translations/ln-translations.schema.json +120 -120
- package/components/ln-ui-coordinator/README.md +62 -62
- package/components/ln-ui-coordinator/ln-ui-coordinator.js +1 -1
- package/components/ln-ui-coordinator/ln-ui-coordinator.schema.json +28 -28
- package/components/ln-ui-coordinator/src/ln-ui-coordinator.js +140 -140
- package/components/ln-upload/ln-upload.js +1 -1
- package/components/ln-upload/ln-upload.schema.json +193 -193
- package/components/ln-upload/src/upload-model.js +78 -78
- package/components/ln-validate/ln-validate.js +1 -1
- package/components/ln-validate/ln-validate.schema.json +50 -50
- package/components/ln-validate/src/validate-model.js +64 -64
- package/components/ln-websocket-connector/README.md +166 -166
- package/components/ln-websocket-connector/ln-websocket-connector.js +1 -1
- package/components/ln-websocket-connector/ln-websocket-connector.schema.json +29 -29
- package/components/ln-websocket-connector/src/ln-websocket-connector.js +283 -279
- package/ln-ashlar.html-data.json +7 -8
- package/package.json +84 -84
- package/theme/brand.css +73 -73
- package/theme/components/_confirm.scss +6 -6
- package/theme/components/_picklist.scss +3 -3
- package/theme/config/_palette.scss +274 -274
- package/theme/config/_theme.scss +292 -292
- package/theme/config/_tokens.scss +285 -285
- package/theme/config/mixins/_index.scss +62 -62
- package/theme/config/mixins/_picklist.scss +66 -66
- package/theme/ln-ashlar-dev.scss +66 -66
- package/theme/ln-ashlar-landing.scss +56 -56
- package/theme/ln-ashlar-theme.scss +63 -63
- package/theme/presentation/_bento.scss +151 -151
- package/theme/presentation/_cta.scss +111 -111
- package/theme/presentation/_effects.scss +216 -216
- package/theme/presentation/_features.scss +108 -108
- package/theme/presentation/_footer.scss +129 -129
- package/theme/presentation/_hero.scss +158 -158
- package/theme/presentation/_layout.scss +170 -170
- package/theme/presentation/_navbar.scss +140 -140
- package/theme/presentation/_pricing.scss +262 -262
- package/theme/presentation/_stats.scss +87 -87
- package/theme/presentation/_testimonials.scss +124 -124
- package/theme/presentation/_tokens.scss +39 -39
- package/theme/presentation/_typography.scss +163 -163
- package/web-types.json +12 -12
|
@@ -1,155 +1,184 @@
|
|
|
1
|
-
import { compareValues, detectValueType } from '../../ln-core/compare.js';
|
|
2
|
-
import { matchesFilterValues, matchesSearchTokens, tokenizeSearchQuery } from '../../ln-core/matching.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Sorts an array of record objects by field and direction using pure core comparison.
|
|
6
|
-
* @param {Array<Record<string, any>>} records
|
|
7
|
-
* @param {{ field?: string, direction?: string } | null} [sort]
|
|
8
|
-
* @param {Intl.Collator} [collator]
|
|
9
|
-
* @returns {Array<Record<string, any>>}
|
|
10
|
-
*/
|
|
11
|
-
export function sortRecords(records, sort, collator) {
|
|
12
|
-
if (!Array.isArray(records) || !sort || !sort.field) return records;
|
|
13
|
-
const { field, direction } = sort;
|
|
14
|
-
const desc = direction === 'desc';
|
|
15
|
-
|
|
16
|
-
// Lock the value type once for the entire sort operation (per Doctrinal / compare.js rules)
|
|
17
|
-
const sampleValues = records.map(r => (r ? r[field] : undefined));
|
|
18
|
-
const valueType = detectValueType(sampleValues);
|
|
19
|
-
|
|
20
|
-
return [...records].sort((a, b) => {
|
|
21
|
-
const va = a ? a[field] : undefined;
|
|
22
|
-
const vb = b ? b[field] : undefined;
|
|
23
|
-
|
|
24
|
-
const result = compareValues(va, vb, valueType, collator);
|
|
25
|
-
return desc ? -result : result;
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Filters an array of records against a dictionary of active filter arrays.
|
|
31
|
-
* Reuses matchesFilterValues from ln-core/matching (AND across fields, OR within field).
|
|
32
|
-
* @param {Array<Record<string, any>>} records
|
|
33
|
-
* @param {Record<string, Array<any>>} filters
|
|
34
|
-
* @returns {Array<Record<string, any>>}
|
|
35
|
-
*/
|
|
36
|
-
export function filterRecords(records, filters) {
|
|
37
|
-
if (!Array.isArray(records) || !filters || typeof filters !== 'object') return records;
|
|
38
|
-
|
|
39
|
-
const keys = Object.keys(filters).filter(k => Array.isArray(filters[k]) && filters[k].length > 0);
|
|
40
|
-
if (!keys.length) return records;
|
|
41
|
-
|
|
42
|
-
return records.filter(record => {
|
|
43
|
-
if (!record) return false;
|
|
44
|
-
return keys.every(field => matchesFilterValues(record[field], filters[field]));
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Searches records across specified fields using ln-core/matching tokenization and matching.
|
|
50
|
-
* @param {Array<Record<string, any>>} records
|
|
51
|
-
* @param {string} query
|
|
52
|
-
* @param {string[]} searchFields
|
|
53
|
-
* @returns {Array<Record<string, any>>}
|
|
54
|
-
*/
|
|
55
|
-
export function searchRecords(records, query, searchFields) {
|
|
56
|
-
if (!Array.isArray(records) || !query || !searchFields || !searchFields.length) return records;
|
|
57
|
-
|
|
58
|
-
const tokens = tokenizeSearchQuery(query);
|
|
59
|
-
if (!tokens.length) return records;
|
|
60
|
-
|
|
61
|
-
return records.filter(record => {
|
|
62
|
-
if (!record) return false;
|
|
63
|
-
return tokens.every(token =>
|
|
64
|
-
searchFields.some(field => {
|
|
65
|
-
const val = record[field];
|
|
66
|
-
return val != null && matchesSearchTokens(String(val), [token]);
|
|
67
|
-
})
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Computes aggregates (count, sum, avg) on a specific field across records.
|
|
74
|
-
* @param {Array<Record<string, any>>} records
|
|
75
|
-
* @param {string} field
|
|
76
|
-
* @param {'count' | 'sum' | 'avg'} fn
|
|
77
|
-
* @returns {number}
|
|
78
|
-
*/
|
|
79
|
-
export function aggregateRecords(records, field, fn) {
|
|
80
|
-
if (!Array.isArray(records) || !records.length) return 0;
|
|
81
|
-
if (fn === 'count') return records.length;
|
|
82
|
-
|
|
83
|
-
const numbers = records
|
|
84
|
-
.map(r => (r && r[field] != null ? parseFloat(r[field]) : NaN))
|
|
85
|
-
.filter(v => Number.isFinite(v));
|
|
86
|
-
|
|
87
|
-
const sum = numbers.reduce((a, b) => a + b, 0);
|
|
88
|
-
|
|
89
|
-
if (fn === 'sum') return sum;
|
|
90
|
-
if (fn === 'avg') return numbers.length ? sum / numbers.length : 0;
|
|
91
|
-
return 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Executes full in-memory query evaluation: filter -> search -> sort -> slice.
|
|
96
|
-
* @param {Array<Record<string, any>>} records
|
|
97
|
-
* @param {{ filters?: Record<string, any[]>, search?: string, sort?: { field?: string, direction?: string }, offset?: number, limit?: number }} [options={}]
|
|
98
|
-
* @param {string[]} [searchFields=[]]
|
|
99
|
-
* @param {Intl.Collator} [collator]
|
|
100
|
-
* @returns {{ records: Array<Record<string, any>>, total: number, filtered: number }}
|
|
101
|
-
*/
|
|
102
|
-
export function queryRecords(records, options = {}, searchFields = [], collator) {
|
|
103
|
-
if (!Array.isArray(records)) {
|
|
104
|
-
return { records: [], total: 0, filtered: 0 };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const total = records.length;
|
|
108
|
-
let result = records;
|
|
109
|
-
|
|
110
|
-
if (options.filters) {
|
|
111
|
-
result = filterRecords(result, options.filters);
|
|
112
|
-
}
|
|
113
|
-
if (options.search) {
|
|
114
|
-
result = searchRecords(result, options.search, searchFields);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const filtered = result.length;
|
|
118
|
-
|
|
119
|
-
if (options.sort) {
|
|
120
|
-
result = sortRecords(result, options.sort, collator);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (options.offset || options.limit) {
|
|
124
|
-
const offset = options.offset || 0;
|
|
125
|
-
const limit = options.limit || result.length;
|
|
126
|
-
result = result.slice(offset, offset + limit);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { records: result, total, filtered };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Decorates records with computed fields (pure, no console side-effects).
|
|
134
|
-
* @param {Array<Record<string, any>>} records
|
|
135
|
-
* @param {Record<string, (record: any) => any>} [computed]
|
|
136
|
-
* @returns {Array<Record<string, any>>}
|
|
137
|
-
*/
|
|
138
|
-
export function decorateRecords(records, computed) {
|
|
139
|
-
if (!Array.isArray(records) || !computed || typeof computed !== 'object') return records;
|
|
140
|
-
|
|
141
|
-
return records.map(record => {
|
|
142
|
-
if (!record) return null;
|
|
143
|
-
const copy = { ...record };
|
|
144
|
-
for (const [fieldName, fn] of Object.entries(computed)) {
|
|
145
|
-
if (typeof fn === 'function') {
|
|
146
|
-
try {
|
|
147
|
-
copy[fieldName] = fn(record);
|
|
148
|
-
} catch (_) {
|
|
149
|
-
copy[fieldName] = undefined;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return copy;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
1
|
+
import { compareValues, detectValueType } from '../../ln-core/compare.js';
|
|
2
|
+
import { matchesFilterValues, matchesSearchTokens, tokenizeSearchQuery } from '../../ln-core/matching.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sorts an array of record objects by field and direction using pure core comparison.
|
|
6
|
+
* @param {Array<Record<string, any>>} records
|
|
7
|
+
* @param {{ field?: string, direction?: string } | null} [sort]
|
|
8
|
+
* @param {Intl.Collator} [collator]
|
|
9
|
+
* @returns {Array<Record<string, any>>}
|
|
10
|
+
*/
|
|
11
|
+
export function sortRecords(records, sort, collator) {
|
|
12
|
+
if (!Array.isArray(records) || !sort || !sort.field) return records;
|
|
13
|
+
const { field, direction } = sort;
|
|
14
|
+
const desc = direction === 'desc';
|
|
15
|
+
|
|
16
|
+
// Lock the value type once for the entire sort operation (per Doctrinal / compare.js rules)
|
|
17
|
+
const sampleValues = records.map(r => (r ? r[field] : undefined));
|
|
18
|
+
const valueType = detectValueType(sampleValues);
|
|
19
|
+
|
|
20
|
+
return [...records].sort((a, b) => {
|
|
21
|
+
const va = a ? a[field] : undefined;
|
|
22
|
+
const vb = b ? b[field] : undefined;
|
|
23
|
+
|
|
24
|
+
const result = compareValues(va, vb, valueType, collator);
|
|
25
|
+
return desc ? -result : result;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Filters an array of records against a dictionary of active filter arrays.
|
|
31
|
+
* Reuses matchesFilterValues from ln-core/matching (AND across fields, OR within field).
|
|
32
|
+
* @param {Array<Record<string, any>>} records
|
|
33
|
+
* @param {Record<string, Array<any>>} filters
|
|
34
|
+
* @returns {Array<Record<string, any>>}
|
|
35
|
+
*/
|
|
36
|
+
export function filterRecords(records, filters) {
|
|
37
|
+
if (!Array.isArray(records) || !filters || typeof filters !== 'object') return records;
|
|
38
|
+
|
|
39
|
+
const keys = Object.keys(filters).filter(k => Array.isArray(filters[k]) && filters[k].length > 0);
|
|
40
|
+
if (!keys.length) return records;
|
|
41
|
+
|
|
42
|
+
return records.filter(record => {
|
|
43
|
+
if (!record) return false;
|
|
44
|
+
return keys.every(field => matchesFilterValues(record[field], filters[field]));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Searches records across specified fields using ln-core/matching tokenization and matching.
|
|
50
|
+
* @param {Array<Record<string, any>>} records
|
|
51
|
+
* @param {string} query
|
|
52
|
+
* @param {string[]} searchFields
|
|
53
|
+
* @returns {Array<Record<string, any>>}
|
|
54
|
+
*/
|
|
55
|
+
export function searchRecords(records, query, searchFields) {
|
|
56
|
+
if (!Array.isArray(records) || !query || !searchFields || !searchFields.length) return records;
|
|
57
|
+
|
|
58
|
+
const tokens = tokenizeSearchQuery(query);
|
|
59
|
+
if (!tokens.length) return records;
|
|
60
|
+
|
|
61
|
+
return records.filter(record => {
|
|
62
|
+
if (!record) return false;
|
|
63
|
+
return tokens.every(token =>
|
|
64
|
+
searchFields.some(field => {
|
|
65
|
+
const val = record[field];
|
|
66
|
+
return val != null && matchesSearchTokens(String(val), [token]);
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Computes aggregates (count, sum, avg) on a specific field across records.
|
|
74
|
+
* @param {Array<Record<string, any>>} records
|
|
75
|
+
* @param {string} field
|
|
76
|
+
* @param {'count' | 'sum' | 'avg'} fn
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
export function aggregateRecords(records, field, fn) {
|
|
80
|
+
if (!Array.isArray(records) || !records.length) return 0;
|
|
81
|
+
if (fn === 'count') return records.length;
|
|
82
|
+
|
|
83
|
+
const numbers = records
|
|
84
|
+
.map(r => (r && r[field] != null ? parseFloat(r[field]) : NaN))
|
|
85
|
+
.filter(v => Number.isFinite(v));
|
|
86
|
+
|
|
87
|
+
const sum = numbers.reduce((a, b) => a + b, 0);
|
|
88
|
+
|
|
89
|
+
if (fn === 'sum') return sum;
|
|
90
|
+
if (fn === 'avg') return numbers.length ? sum / numbers.length : 0;
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Executes full in-memory query evaluation: filter -> search -> sort -> slice.
|
|
96
|
+
* @param {Array<Record<string, any>>} records
|
|
97
|
+
* @param {{ filters?: Record<string, any[]>, search?: string, sort?: { field?: string, direction?: string }, offset?: number, limit?: number }} [options={}]
|
|
98
|
+
* @param {string[]} [searchFields=[]]
|
|
99
|
+
* @param {Intl.Collator} [collator]
|
|
100
|
+
* @returns {{ records: Array<Record<string, any>>, total: number, filtered: number }}
|
|
101
|
+
*/
|
|
102
|
+
export function queryRecords(records, options = {}, searchFields = [], collator) {
|
|
103
|
+
if (!Array.isArray(records)) {
|
|
104
|
+
return { records: [], total: 0, filtered: 0 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const total = records.length;
|
|
108
|
+
let result = records;
|
|
109
|
+
|
|
110
|
+
if (options.filters) {
|
|
111
|
+
result = filterRecords(result, options.filters);
|
|
112
|
+
}
|
|
113
|
+
if (options.search) {
|
|
114
|
+
result = searchRecords(result, options.search, searchFields);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filtered = result.length;
|
|
118
|
+
|
|
119
|
+
if (options.sort) {
|
|
120
|
+
result = sortRecords(result, options.sort, collator);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.offset || options.limit) {
|
|
124
|
+
const offset = options.offset || 0;
|
|
125
|
+
const limit = options.limit || result.length;
|
|
126
|
+
result = result.slice(offset, offset + limit);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { records: result, total, filtered };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decorates records with computed fields (pure, no console side-effects).
|
|
134
|
+
* @param {Array<Record<string, any>>} records
|
|
135
|
+
* @param {Record<string, (record: any) => any>} [computed]
|
|
136
|
+
* @returns {Array<Record<string, any>>}
|
|
137
|
+
*/
|
|
138
|
+
export function decorateRecords(records, computed) {
|
|
139
|
+
if (!Array.isArray(records) || !computed || typeof computed !== 'object') return records;
|
|
140
|
+
|
|
141
|
+
return records.map(record => {
|
|
142
|
+
if (!record) return null;
|
|
143
|
+
const copy = { ...record };
|
|
144
|
+
for (const [fieldName, fn] of Object.entries(computed)) {
|
|
145
|
+
if (typeof fn === 'function') {
|
|
146
|
+
try {
|
|
147
|
+
copy[fieldName] = fn(record);
|
|
148
|
+
} catch (_) {
|
|
149
|
+
copy[fieldName] = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return copy;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Pure helper to compute whether an IndexedDB upgrade is needed.
|
|
159
|
+
* @param {string[]} existingStoreNames
|
|
160
|
+
* @param {Record<string, { indexes: string[] }>} requiredStores
|
|
161
|
+
* @param {Record<string, string[]>} existingIndexMap
|
|
162
|
+
* @param {string} [metaStoreName='_meta']
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
export function checkNeedsUpgrade(existingStoreNames, requiredStores, existingIndexMap, metaStoreName = '_meta') {
|
|
166
|
+
if (!Array.isArray(existingStoreNames) || !existingStoreNames.includes(metaStoreName)) return true;
|
|
167
|
+
for (const name of Object.keys(requiredStores || {})) {
|
|
168
|
+
if (!existingStoreNames.includes(name)) return true;
|
|
169
|
+
const requiredIdxs = (requiredStores[name] && requiredStores[name].indexes) || [];
|
|
170
|
+
const existingIdxs = (existingIndexMap && existingIndexMap[name]) || [];
|
|
171
|
+
if (requiredIdxs.some(idx => !existingIdxs.includes(idx))) return true;
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Pure helper to normalize synchronization errors into a standard response shape.
|
|
178
|
+
* @param {any} err
|
|
179
|
+
* @returns {{ ok: false, error: string }}
|
|
180
|
+
*/
|
|
181
|
+
export function normalizeSyncError(err) {
|
|
182
|
+
const errorMsg = (err && err.message) ? err.message : String(err);
|
|
183
|
+
return { ok: false, error: errorMsg };
|
|
184
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerComponent, dispatch, setCryptoKey, getCryptoKey, encryptData, decryptData, defineAttrs, attrSpec, attrInt, attrBool, attrList } from '../../ln-core';
|
|
2
2
|
import { createWindowIndex } from './window-index.js';
|
|
3
|
-
import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from './data-store-model.js';
|
|
3
|
+
import { aggregateRecords, decorateRecords, filterRecords, queryRecords, checkNeedsUpgrade, normalizeSyncError } from './data-store-model.js';
|
|
4
4
|
|
|
5
5
|
(function () {
|
|
6
6
|
const DOM_SELECTOR = 'data-ln-data-store';
|
|
@@ -66,7 +66,7 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
66
66
|
|
|
67
67
|
_dbReady = new Promise(resolve => {
|
|
68
68
|
if (typeof indexedDB === 'undefined') {
|
|
69
|
-
console.warn('[ln-data-store] IndexedDB not available
|
|
69
|
+
console.warn('[ln-data-store] IndexedDB is not available in this environment');
|
|
70
70
|
return resolve(null);
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -75,14 +75,24 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
75
75
|
const probe = indexedDB.open(DB_NAME);
|
|
76
76
|
|
|
77
77
|
probe.onerror = () => {
|
|
78
|
-
console.warn('[ln-data-store] IndexedDB open failed
|
|
78
|
+
console.warn('[ln-data-store] IndexedDB open failed');
|
|
79
79
|
resolve(null);
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
probe.onsuccess = e => {
|
|
83
83
|
const db = e.target.result;
|
|
84
84
|
const existing = Array.from(db.objectStoreNames);
|
|
85
|
-
const
|
|
85
|
+
const existingRequired = requiredNames.filter(name => existing.includes(name));
|
|
86
|
+
|
|
87
|
+
let existingIndexMap = {};
|
|
88
|
+
if (existingRequired.length > 0) {
|
|
89
|
+
const tx = db.transaction(existingRequired, 'readonly');
|
|
90
|
+
for (const name of existingRequired) {
|
|
91
|
+
existingIndexMap[name] = Array.from(tx.objectStore(name).indexNames);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const needsUpgrade = checkNeedsUpgrade(existing, required, existingIndexMap, META_STORE);
|
|
86
96
|
|
|
87
97
|
if (!needsUpgrade) {
|
|
88
98
|
_setupVersionChangeHandler(db);
|
|
@@ -94,18 +104,35 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
94
104
|
db.close();
|
|
95
105
|
|
|
96
106
|
const upgrade = indexedDB.open(DB_NAME, currentVersion + 1);
|
|
107
|
+
let settled = false;
|
|
108
|
+
let blockedTimeout = null;
|
|
97
109
|
|
|
98
110
|
upgrade.onblocked = () => {
|
|
99
111
|
console.warn('[ln-data-store] Database upgrade blocked — waiting for other tabs to close connection');
|
|
112
|
+
dispatch(document, 'ln-data-store:blocked', { db: DB_NAME });
|
|
113
|
+
if (!blockedTimeout) {
|
|
114
|
+
blockedTimeout = setTimeout(() => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
console.warn('[ln-data-store] Database upgrade timed out while blocked');
|
|
117
|
+
settled = true;
|
|
118
|
+
_dbReady = null;
|
|
119
|
+
resolve(null);
|
|
120
|
+
}, 5000);
|
|
121
|
+
}
|
|
100
122
|
};
|
|
101
123
|
|
|
102
124
|
upgrade.onerror = () => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
if (blockedTimeout) clearTimeout(blockedTimeout);
|
|
127
|
+
settled = true;
|
|
128
|
+
_dbReady = null;
|
|
103
129
|
console.warn('[ln-data-store] Database upgrade failed');
|
|
104
130
|
resolve(null);
|
|
105
131
|
};
|
|
106
132
|
|
|
107
133
|
upgrade.onupgradeneeded = e => {
|
|
108
134
|
const db = e.target.result;
|
|
135
|
+
const tx = e.target.transaction;
|
|
109
136
|
if (!db.objectStoreNames.contains(META_STORE)) {
|
|
110
137
|
db.createObjectStore(META_STORE, { keyPath: 'key' });
|
|
111
138
|
}
|
|
@@ -115,11 +142,24 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
115
142
|
for (const idx of required[storeName].indexes) {
|
|
116
143
|
store.createIndex(idx, idx, { unique: false });
|
|
117
144
|
}
|
|
145
|
+
} else {
|
|
146
|
+
const store = tx.objectStore(storeName);
|
|
147
|
+
for (const idx of required[storeName].indexes) {
|
|
148
|
+
if (!store.indexNames.contains(idx)) {
|
|
149
|
+
store.createIndex(idx, idx, { unique: false });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
118
152
|
}
|
|
119
153
|
}
|
|
120
154
|
};
|
|
121
155
|
|
|
122
156
|
upgrade.onsuccess = e => {
|
|
157
|
+
if (settled) {
|
|
158
|
+
if (e.target.result) e.target.result.close();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (blockedTimeout) clearTimeout(blockedTimeout);
|
|
162
|
+
settled = true;
|
|
123
163
|
const db = e.target.result;
|
|
124
164
|
_setupVersionChangeHandler(db);
|
|
125
165
|
_db = db;
|
|
@@ -771,7 +811,7 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
771
811
|
_component.prototype.count = function (filters) {
|
|
772
812
|
const hasFilters = filters && Object.keys(filters).length > 0;
|
|
773
813
|
if (!hasFilters) {
|
|
774
|
-
if (this.totalCount != null) return Promise.resolve(this.totalCount);
|
|
814
|
+
if (this.windowed && this.totalCount != null) return Promise.resolve(this.totalCount);
|
|
775
815
|
return _countRecords(this._name);
|
|
776
816
|
}
|
|
777
817
|
return _getAllRecords(this._name).then(records => filterRecords(records, filters).length);
|
|
@@ -791,12 +831,12 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
791
831
|
meta = meta || {};
|
|
792
832
|
const self = this;
|
|
793
833
|
if (self._windowIndex && meta.queryGen != null && meta.queryGen !== self._windowIndex.queryGen) {
|
|
794
|
-
return Promise.resolve();
|
|
834
|
+
return Promise.resolve({ ok: true, stale: true });
|
|
795
835
|
}
|
|
796
836
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
837
|
+
let chain = _getDb().then(db => {
|
|
838
|
+
if (!db) throw new Error('IndexedDB unavailable');
|
|
839
|
+
});
|
|
800
840
|
if (upsertedRecords.length > 0) chain = chain.then(() => _putBulk(self._name, upsertedRecords));
|
|
801
841
|
if (deletedIds.length > 0) chain = chain.then(() => _deleteBulk(self._name, deletedIds));
|
|
802
842
|
|
|
@@ -836,9 +876,16 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
836
876
|
meta: meta
|
|
837
877
|
});
|
|
838
878
|
}
|
|
879
|
+
return { ok: true };
|
|
839
880
|
}).catch(err => {
|
|
840
881
|
self.isSyncing = false;
|
|
882
|
+
const errorMsg = (err && err.message) ? err.message : String(err);
|
|
841
883
|
console.error('[ln-data-store] applySync failed:', err);
|
|
884
|
+
dispatch(self.dom, 'ln-data-store:sync-error', {
|
|
885
|
+
store: self._name,
|
|
886
|
+
error: errorMsg
|
|
887
|
+
});
|
|
888
|
+
return normalizeSyncError(err);
|
|
842
889
|
});
|
|
843
890
|
};
|
|
844
891
|
|
|
@@ -957,14 +1004,14 @@ import { aggregateRecords, decorateRecords, filterRecords, queryRecords } from '
|
|
|
957
1004
|
|
|
958
1005
|
// The IndexedDB schema and the store's registered name are fixed at open
|
|
959
1006
|
// time: createIndex is only legal inside a versionchange transaction
|
|
960
|
-
// (see onupgradeneeded
|
|
1007
|
+
// (see onupgradeneeded). A post-init edit is recorded on the host
|
|
961
1008
|
// for the dev stylesheet to surface; nothing is applied.
|
|
962
1009
|
function _markFrozen(el, attrName) {
|
|
963
1010
|
el.setAttribute(FROZEN_ATTR, attrName);
|
|
964
1011
|
}
|
|
965
1012
|
|
|
966
1013
|
// Shrinking the window drops positions; the records behind them stop being
|
|
967
|
-
// held, exactly as on ingest
|
|
1014
|
+
// held, exactly as on ingest, so they leave IndexedDB too.
|
|
968
1015
|
function _applyWindowSize(el) {
|
|
969
1016
|
const inst = el[DOM_ATTRIBUTE];
|
|
970
1017
|
if (!inst._windowIndex) return;
|
|
@@ -207,3 +207,7 @@ On blur: empty → clear + dispatch; unchanged from the current formatted displa
|
|
|
207
207
|
### Observers
|
|
208
208
|
|
|
209
209
|
A shared `document.body` `MutationObserver` auto-initializes new `[data-ln-date]` elements (`childList`) and re-initializes on `data-ln-date` attribute addition (`attributes`). A separate observer on `document.documentElement` watches `lang` and re-formats every active instance when it changes.
|
|
210
|
+
|
|
211
|
+
### Teardown & Lifecycle (`destroy()`)
|
|
212
|
+
|
|
213
|
+
When an element is destroyed, the component cleanly detaches the global `ln-core:locale-change` event listener regardless of whether the element is an input decorator or a static text display element (`<time>`, `<td>`, `<span>`). For inputs, wrapper children, blur/click listeners, and value interceptors are unhooked to prevent DOM leaks.
|