@keenmate/pure-admin-core 2.9.0-rc03 → 2.9.0-rc05
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/README.md +55 -13
- package/dist/css/main.css +554 -7
- package/package.json +3 -1
- package/snippets/buttons.html +51 -0
- package/snippets/cards.html +132 -47
- package/snippets/comparison.html +26 -22
- package/snippets/manifest.json +180 -115
- package/snippets/range-group.html +125 -0
- package/snippets/splitter.html +44 -38
- package/snippets/statistics.html +31 -0
- package/src/js/btn-split-auto-absorb.js +327 -0
- package/src/js/command-palette.js +472 -0
- package/src/js/file-selector.js +1275 -0
- package/src/js/internal/logging.js +121 -0
- package/src/js/logic-tree-renderer.js +303 -0
- package/src/js/modal-dialogs.js +460 -0
- package/src/js/overflow.js +371 -0
- package/src/js/pa-stat-fit.js +184 -0
- package/src/js/range-group.js +663 -0
- package/src/js/search-autocomplete-v2.js +907 -0
- package/src/js/search-autocomplete.js +434 -0
- package/src/js/settings-panel.js +245 -0
- package/src/js/split-button.js +141 -0
- package/src/js/splitter.js +1323 -0
- package/src/js/toast-service.js +302 -0
- package/src/js/tooltips-popovers.js +275 -0
- package/src/js/virtual-scroll.js +143 -0
- package/src/js/virtual-textbox.js +803 -0
- package/src/scss/_core.scss +7 -0
- package/src/scss/core-components/_buttons.scss +44 -0
- package/src/scss/core-components/_cards.scss +95 -6
- package/src/scss/core-components/_overflow.scss +50 -0
- package/src/scss/core-components/_range-group.scss +474 -0
- package/src/scss/core-components/_statistics.scss +163 -0
- package/src/scss/variables/_components.scss +41 -2
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Field Autocomplete with Token-Based Query Builder
|
|
3
|
+
* Provides field name suggestions when user types ":"
|
|
4
|
+
* Then provides operator suggestions based on field type
|
|
5
|
+
* Resolved fragments appear as token badges
|
|
6
|
+
* Example: :name starts_with abc → [name][starts_with][abc]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class SearchAutocomplete {
|
|
10
|
+
constructor(inputElement, fields, options = {}) {
|
|
11
|
+
this.input = inputElement;
|
|
12
|
+
this.fields = fields;
|
|
13
|
+
this.popup = null;
|
|
14
|
+
this.tokensContainer = null;
|
|
15
|
+
this.activeIndex = -1;
|
|
16
|
+
this.isOpen = false;
|
|
17
|
+
this.resolvedTokens = []; // { type: 'field'|'operator'|'value', value: string, variant: string }
|
|
18
|
+
this.currentQueryState = 'idle'; // 'idle' | 'expecting-operator' | 'expecting-value'
|
|
19
|
+
this.autocompleteResults = [];
|
|
20
|
+
|
|
21
|
+
// Operator definitions by field type with shortcuts
|
|
22
|
+
this.operators = {
|
|
23
|
+
text: [
|
|
24
|
+
{ value: 'equals', label: 'Equals', shortcuts: ['eq', '='], description: 'Exact match' },
|
|
25
|
+
{ value: 'not_equals', label: 'Not Equals', shortcuts: ['neq', 'ne', '!='], description: 'Does not match' },
|
|
26
|
+
{ value: 'starts_with', label: 'Starts With', shortcuts: ['sw', 'startswith'], description: 'Starts with text' },
|
|
27
|
+
{ value: 'ends_with', label: 'Ends With', shortcuts: ['ew', 'endswith'], description: 'Ends with text' },
|
|
28
|
+
{ value: 'contains', label: 'Contains', shortcuts: ['c', 'has'], description: 'Contains text' },
|
|
29
|
+
{ value: 'not_contains', label: 'Not Contains', shortcuts: ['nc', 'notcontains'], description: 'Does not contain' }
|
|
30
|
+
],
|
|
31
|
+
number: [
|
|
32
|
+
{ value: '=', label: 'Equals', shortcuts: ['eq', '=='], description: 'Exact value' },
|
|
33
|
+
{ value: '!=', label: 'Not Equals', shortcuts: ['neq', 'ne'], description: 'Different value' },
|
|
34
|
+
{ value: '>', label: 'Greater Than', shortcuts: ['gt'], description: 'Greater than value' },
|
|
35
|
+
{ value: '<', label: 'Less Than', shortcuts: ['lt'], description: 'Less than value' },
|
|
36
|
+
{ value: 'between', label: 'Between', shortcuts: ['bw', 'btw'], description: 'Between two values' },
|
|
37
|
+
{ value: '>=', label: 'Greater or Equal', shortcuts: ['gte', 'ge'], description: 'Greater or equal' },
|
|
38
|
+
{ value: '<=', label: 'Less or Equal', shortcuts: ['lte', 'le'], description: 'Less or equal' }
|
|
39
|
+
],
|
|
40
|
+
date: [
|
|
41
|
+
{ value: 'equals', label: 'On Date', shortcuts: ['eq', '=', 'on'], description: 'Exact date' },
|
|
42
|
+
{ value: 'before', label: 'Before', shortcuts: ['bf', '<'], description: 'Before date' },
|
|
43
|
+
{ value: 'after', label: 'After', shortcuts: ['af', '>'], description: 'After date' },
|
|
44
|
+
{ value: 'between', label: 'Between', shortcuts: ['bw', 'btw'], description: 'Date range' },
|
|
45
|
+
{ value: 'in_last', label: 'In Last', shortcuts: ['last', 'past'], description: 'In last X days' },
|
|
46
|
+
{ value: 'in_next', label: 'In Next', shortcuts: ['next', 'future'], description: 'In next X days' }
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.init();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
init() {
|
|
54
|
+
// Find or create tokens container
|
|
55
|
+
const wrapper = this.input.parentElement;
|
|
56
|
+
this.tokensContainer = wrapper.querySelector('.pa-search-tokens');
|
|
57
|
+
|
|
58
|
+
if (!this.tokensContainer) {
|
|
59
|
+
this.tokensContainer = document.createElement('div');
|
|
60
|
+
this.tokensContainer.className = 'pa-search-tokens';
|
|
61
|
+
wrapper.insertBefore(this.tokensContainer, this.input);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create popup element
|
|
65
|
+
this.popup = document.createElement('div');
|
|
66
|
+
this.popup.className = 'pa-search-autocomplete';
|
|
67
|
+
this.popup.style.display = 'none';
|
|
68
|
+
document.body.appendChild(this.popup);
|
|
69
|
+
|
|
70
|
+
// Event listeners
|
|
71
|
+
this.input.addEventListener('input', (e) => this.handleInput(e));
|
|
72
|
+
this.input.addEventListener('keydown', (e) => this.handleKeydown(e));
|
|
73
|
+
document.addEventListener('click', (e) => this.handleClickOutside(e));
|
|
74
|
+
|
|
75
|
+
// Initialize state
|
|
76
|
+
this.updateQueryState();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update query state based on number of tokens
|
|
81
|
+
*/
|
|
82
|
+
updateQueryState() {
|
|
83
|
+
const tokenCount = this.resolvedTokens.length % 3;
|
|
84
|
+
|
|
85
|
+
if (tokenCount === 0) {
|
|
86
|
+
this.currentQueryState = 'idle';
|
|
87
|
+
} else if (tokenCount === 1) {
|
|
88
|
+
this.currentQueryState = 'expecting-operator';
|
|
89
|
+
} else if (tokenCount === 2) {
|
|
90
|
+
this.currentQueryState = 'expecting-value';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Search for autocomplete suggestions
|
|
96
|
+
*/
|
|
97
|
+
searchAutocomplete(query) {
|
|
98
|
+
if (this.currentQueryState === 'idle') {
|
|
99
|
+
// Search for fields (must start with :)
|
|
100
|
+
if (query.startsWith(':')) {
|
|
101
|
+
const searchTerm = query.substring(1).toLowerCase();
|
|
102
|
+
return this.fields.filter(field =>
|
|
103
|
+
field.name.toLowerCase().includes(searchTerm)
|
|
104
|
+
).map(field => ({
|
|
105
|
+
value: field.name,
|
|
106
|
+
label: field.name,
|
|
107
|
+
description: `${field.type} field`,
|
|
108
|
+
type: field.type
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
} else if (this.currentQueryState === 'expecting-operator') {
|
|
112
|
+
// Get the last resolved field token
|
|
113
|
+
const lastFieldToken = this.resolvedTokens[this.resolvedTokens.length - 1];
|
|
114
|
+
const fieldDef = this.fields.find(f => f.name === lastFieldToken.value);
|
|
115
|
+
|
|
116
|
+
if (!fieldDef) return [];
|
|
117
|
+
|
|
118
|
+
const operators = this.operators[fieldDef.type] || [];
|
|
119
|
+
const searchTerm = query.toLowerCase();
|
|
120
|
+
|
|
121
|
+
return operators.filter(op =>
|
|
122
|
+
searchTerm === '' ||
|
|
123
|
+
op.value.toLowerCase().includes(searchTerm) ||
|
|
124
|
+
op.label.toLowerCase().includes(searchTerm) ||
|
|
125
|
+
(op.shortcuts && op.shortcuts.some(s => s.toLowerCase().includes(searchTerm)))
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render autocomplete dropdown
|
|
134
|
+
*/
|
|
135
|
+
renderAutocomplete() {
|
|
136
|
+
if (this.autocompleteResults.length === 0) {
|
|
137
|
+
this.close();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isFieldSearch = this.currentQueryState === 'idle';
|
|
142
|
+
let html = '';
|
|
143
|
+
|
|
144
|
+
this.autocompleteResults.forEach((item, index) => {
|
|
145
|
+
const isActive = index === this.activeIndex;
|
|
146
|
+
const icon = isFieldSearch ? '🔍' : '⚡';
|
|
147
|
+
|
|
148
|
+
html += `
|
|
149
|
+
<div class="pa-search-autocomplete__item ${isActive ? 'pa-search-autocomplete__item--active' : ''}" data-index="${index}">
|
|
150
|
+
<span class="pa-search-autocomplete__item-icon">${icon}</span>
|
|
151
|
+
<div class="pa-search-autocomplete__item-content">
|
|
152
|
+
<span class="pa-search-autocomplete__item-name">${item.label}</span>
|
|
153
|
+
<span class="pa-search-autocomplete__item-type">${item.description}</span>
|
|
154
|
+
</div>
|
|
155
|
+
<span class="pa-search-autocomplete__item-badge">${item.value}</span>
|
|
156
|
+
</div>
|
|
157
|
+
`;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this.popup.innerHTML = html;
|
|
161
|
+
|
|
162
|
+
// Add click handlers
|
|
163
|
+
this.popup.querySelectorAll('.pa-search-autocomplete__item').forEach((item, index) => {
|
|
164
|
+
item.addEventListener('click', () => {
|
|
165
|
+
this.selectAutocomplete(this.autocompleteResults[index]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.position();
|
|
170
|
+
this.open();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Select autocomplete item
|
|
175
|
+
*/
|
|
176
|
+
selectAutocomplete(item) {
|
|
177
|
+
if (!item) return;
|
|
178
|
+
|
|
179
|
+
// Determine token type and variant
|
|
180
|
+
let tokenType, variant;
|
|
181
|
+
|
|
182
|
+
if (this.currentQueryState === 'idle') {
|
|
183
|
+
tokenType = 'field';
|
|
184
|
+
variant = 'primary';
|
|
185
|
+
} else if (this.currentQueryState === 'expecting-operator') {
|
|
186
|
+
tokenType = 'operator';
|
|
187
|
+
variant = 'secondary';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add token
|
|
191
|
+
this.resolvedTokens.push({
|
|
192
|
+
type: tokenType,
|
|
193
|
+
value: item.value,
|
|
194
|
+
variant: variant
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Update state
|
|
198
|
+
this.updateQueryState();
|
|
199
|
+
|
|
200
|
+
// Clear input and autocomplete
|
|
201
|
+
this.input.value = '';
|
|
202
|
+
this.autocompleteResults = [];
|
|
203
|
+
this.activeIndex = -1;
|
|
204
|
+
|
|
205
|
+
// Re-render
|
|
206
|
+
this.renderTokens();
|
|
207
|
+
this.close();
|
|
208
|
+
this.input.focus();
|
|
209
|
+
|
|
210
|
+
// If we just selected a field, automatically show operators
|
|
211
|
+
if (tokenType === 'field') {
|
|
212
|
+
// Trigger operator autocomplete immediately
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
this.autocompleteResults = this.searchAutocomplete('');
|
|
215
|
+
this.activeIndex = this.autocompleteResults.length > 0 ? 0 : -1;
|
|
216
|
+
if (this.autocompleteResults.length > 0) {
|
|
217
|
+
this.renderAutocomplete();
|
|
218
|
+
}
|
|
219
|
+
}, 0);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve value token (when user presses Enter or Tab)
|
|
225
|
+
*/
|
|
226
|
+
resolveValueToken() {
|
|
227
|
+
const value = this.input.value.trim();
|
|
228
|
+
|
|
229
|
+
if (value && this.currentQueryState === 'expecting-value') {
|
|
230
|
+
this.resolvedTokens.push({
|
|
231
|
+
type: 'value',
|
|
232
|
+
value: value,
|
|
233
|
+
variant: 'success'
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this.updateQueryState();
|
|
237
|
+
this.input.value = '';
|
|
238
|
+
this.renderTokens();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Render tokens as badge groups
|
|
244
|
+
*/
|
|
245
|
+
renderTokens() {
|
|
246
|
+
this.tokensContainer.innerHTML = '';
|
|
247
|
+
|
|
248
|
+
// Group tokens into sets of 3 (field, operator, value)
|
|
249
|
+
for (let i = 0; i < this.resolvedTokens.length; i += 3) {
|
|
250
|
+
const queryTokens = this.resolvedTokens.slice(i, i + 3);
|
|
251
|
+
|
|
252
|
+
// Create group container
|
|
253
|
+
const group = document.createElement('div');
|
|
254
|
+
group.className = 'pa-search-token-group';
|
|
255
|
+
|
|
256
|
+
queryTokens.forEach((token, index) => {
|
|
257
|
+
const badge = document.createElement('span');
|
|
258
|
+
badge.className = `pa-badge pa-badge--${token.variant}`;
|
|
259
|
+
badge.textContent = token.value;
|
|
260
|
+
badge.style.margin = '0';
|
|
261
|
+
badge.style.borderRadius = '0';
|
|
262
|
+
|
|
263
|
+
// Round corners on first and last badges
|
|
264
|
+
if (index === 0) {
|
|
265
|
+
badge.style.borderTopLeftRadius = '4px';
|
|
266
|
+
badge.style.borderBottomLeftRadius = '4px';
|
|
267
|
+
}
|
|
268
|
+
if (index === queryTokens.length - 1 || i + index === this.resolvedTokens.length - 1) {
|
|
269
|
+
badge.style.borderTopRightRadius = '4px';
|
|
270
|
+
badge.style.borderBottomRightRadius = '4px';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
group.appendChild(badge);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Add remove button
|
|
277
|
+
const removeBtn = document.createElement('button');
|
|
278
|
+
removeBtn.className = 'pa-search-token-remove';
|
|
279
|
+
removeBtn.innerHTML = '×';
|
|
280
|
+
removeBtn.onclick = (e) => {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
e.stopPropagation();
|
|
283
|
+
this.removeTokenGroup(i);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
group.appendChild(removeBtn);
|
|
287
|
+
this.tokensContainer.appendChild(group);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Remove a token group
|
|
293
|
+
*/
|
|
294
|
+
removeTokenGroup(startIndex) {
|
|
295
|
+
this.resolvedTokens.splice(startIndex, 3);
|
|
296
|
+
this.updateQueryState();
|
|
297
|
+
this.renderTokens();
|
|
298
|
+
this.input.focus();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Handle input changes
|
|
303
|
+
*/
|
|
304
|
+
handleInput(e) {
|
|
305
|
+
const inputValue = this.input.value;
|
|
306
|
+
|
|
307
|
+
// Check for autocomplete
|
|
308
|
+
if (this.currentQueryState === 'idle' || this.currentQueryState === 'expecting-operator') {
|
|
309
|
+
this.autocompleteResults = this.searchAutocomplete(inputValue);
|
|
310
|
+
this.activeIndex = this.autocompleteResults.length > 0 ? 0 : -1;
|
|
311
|
+
|
|
312
|
+
if (this.autocompleteResults.length > 0) {
|
|
313
|
+
this.renderAutocomplete();
|
|
314
|
+
} else {
|
|
315
|
+
this.close();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Handle keyboard navigation
|
|
322
|
+
*/
|
|
323
|
+
handleKeydown(e) {
|
|
324
|
+
// Handle autocomplete navigation
|
|
325
|
+
if (this.autocompleteResults.length > 0 && this.isOpen) {
|
|
326
|
+
if (e.key === 'ArrowUp') {
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
this.navigateAutocompletePrevious();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (e.key === 'ArrowDown') {
|
|
332
|
+
e.preventDefault();
|
|
333
|
+
this.navigateAutocompleteNext();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (e.key === 'Enter' || e.key === 'Tab') {
|
|
337
|
+
e.preventDefault();
|
|
338
|
+
this.selectAutocomplete(this.autocompleteResults[this.activeIndex]);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Handle value resolution
|
|
344
|
+
if (this.currentQueryState === 'expecting-value' && (e.key === 'Enter' || e.key === 'Tab')) {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
this.resolveValueToken();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Backspace at start - remove last token
|
|
351
|
+
if (e.key === 'Backspace' && this.input.value === '' && this.resolvedTokens.length > 0) {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
this.resolvedTokens.pop();
|
|
354
|
+
this.updateQueryState();
|
|
355
|
+
this.renderTokens();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Escape to close
|
|
360
|
+
if (e.key === 'Escape') {
|
|
361
|
+
this.close();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Navigate autocomplete suggestions
|
|
367
|
+
*/
|
|
368
|
+
navigateAutocompletePrevious() {
|
|
369
|
+
if (this.autocompleteResults.length === 0) return;
|
|
370
|
+
|
|
371
|
+
this.activeIndex = this.activeIndex <= 0
|
|
372
|
+
? this.autocompleteResults.length - 1
|
|
373
|
+
: this.activeIndex - 1;
|
|
374
|
+
|
|
375
|
+
this.renderAutocomplete();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
navigateAutocompleteNext() {
|
|
379
|
+
if (this.autocompleteResults.length === 0) return;
|
|
380
|
+
|
|
381
|
+
this.activeIndex = this.activeIndex >= this.autocompleteResults.length - 1
|
|
382
|
+
? 0
|
|
383
|
+
: this.activeIndex + 1;
|
|
384
|
+
|
|
385
|
+
this.renderAutocomplete();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Position popup below input
|
|
390
|
+
*/
|
|
391
|
+
position() {
|
|
392
|
+
const inputRect = this.input.getBoundingClientRect();
|
|
393
|
+
this.popup.style.position = 'absolute';
|
|
394
|
+
this.popup.style.left = inputRect.left + 'px';
|
|
395
|
+
this.popup.style.top = (inputRect.bottom + 4) + 'px';
|
|
396
|
+
this.popup.style.minWidth = Math.max(inputRect.width, 250) + 'px';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Open popup
|
|
401
|
+
*/
|
|
402
|
+
open() {
|
|
403
|
+
this.popup.style.display = 'block';
|
|
404
|
+
this.isOpen = true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Close popup
|
|
409
|
+
*/
|
|
410
|
+
close() {
|
|
411
|
+
this.popup.style.display = 'none';
|
|
412
|
+
this.isOpen = false;
|
|
413
|
+
this.activeIndex = -1;
|
|
414
|
+
this.autocompleteResults = [];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle click outside
|
|
419
|
+
*/
|
|
420
|
+
handleClickOutside(e) {
|
|
421
|
+
if (this.isOpen && !this.popup.contains(e.target) && e.target !== this.input) {
|
|
422
|
+
this.close();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Destroy instance
|
|
428
|
+
*/
|
|
429
|
+
destroy() {
|
|
430
|
+
if (this.popup && this.popup.parentNode) {
|
|
431
|
+
this.popup.parentNode.removeChild(this.popup);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Admin Settings Panel
|
|
3
|
+
* Global settings management for theme, layout, sidebar, fonts, and display options
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function() {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// Wait for DOM to be ready
|
|
10
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
11
|
+
// Settings Panel Elements
|
|
12
|
+
const settingsPanel = document.getElementById('settingsPanel');
|
|
13
|
+
const settingsToggle = document.getElementById('settingsToggle');
|
|
14
|
+
|
|
15
|
+
// Check if elements exist
|
|
16
|
+
if (!settingsPanel || !settingsToggle) {
|
|
17
|
+
console.error('Settings panel elements not found');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const themeSelector = document.getElementById('themeSelector');
|
|
22
|
+
const fontSizeSelector = document.getElementById('fontSizeSelector');
|
|
23
|
+
const fontFamilySelector = document.getElementById('fontFamilySelector');
|
|
24
|
+
const sidebarCollapsed = document.getElementById('sidebarCollapsed');
|
|
25
|
+
const sidebarBehaviorSelector = document.getElementById('sidebarBehaviorSelector');
|
|
26
|
+
const sidebarModeSelector = document.getElementById('sidebarModeSelector');
|
|
27
|
+
const compactMode = document.getElementById('compactMode');
|
|
28
|
+
const containerWidthSelector = document.getElementById('containerWidthSelector');
|
|
29
|
+
const resetSettings = document.getElementById('resetSettings');
|
|
30
|
+
const body = document.body;
|
|
31
|
+
|
|
32
|
+
// Load saved settings
|
|
33
|
+
const loadSettings = () => {
|
|
34
|
+
// Theme - use server-provided currentTheme from global config
|
|
35
|
+
const currentTheme = window.PURE_ADMIN_CONFIG?.currentTheme || 'audi';
|
|
36
|
+
themeSelector.value = currentTheme;
|
|
37
|
+
|
|
38
|
+
// Font size
|
|
39
|
+
const savedFontSize = localStorage.getItem('font-size') || 'default';
|
|
40
|
+
fontSizeSelector.value = savedFontSize;
|
|
41
|
+
document.documentElement.classList.remove('font-size-small', 'font-size-default', 'font-size-large', 'font-size-xlarge');
|
|
42
|
+
if (savedFontSize !== 'default') {
|
|
43
|
+
document.documentElement.classList.add(`font-size-${savedFontSize}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Font family
|
|
47
|
+
const savedFontFamily = localStorage.getItem('font-family') || 'default';
|
|
48
|
+
fontFamilySelector.value = savedFontFamily;
|
|
49
|
+
body.classList.remove('font-family-serif', 'font-family-mono');
|
|
50
|
+
if (savedFontFamily !== 'default') {
|
|
51
|
+
body.classList.add(`font-family-${savedFontFamily}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Sidebar collapsed
|
|
55
|
+
const isSidebarCollapsed = localStorage.getItem('sidebar-hidden') === 'true';
|
|
56
|
+
sidebarCollapsed.checked = isSidebarCollapsed;
|
|
57
|
+
|
|
58
|
+
// Sidebar behavior
|
|
59
|
+
const sidebarBehavior = localStorage.getItem('sidebar-behavior') || 'hide';
|
|
60
|
+
sidebarBehaviorSelector.value = sidebarBehavior;
|
|
61
|
+
const sidebar = document.querySelector('.pa-layout__sidebar');
|
|
62
|
+
const burgerMenu = document.querySelector('.burger-menu');
|
|
63
|
+
if (sidebar) {
|
|
64
|
+
sidebar.classList.remove('pa-layout__sidebar--icon-collapse');
|
|
65
|
+
if (sidebarBehavior === 'icon-collapse') {
|
|
66
|
+
sidebar.classList.add('pa-layout__sidebar--icon-collapse');
|
|
67
|
+
// In icon-collapse mode, check if expanded or collapsed
|
|
68
|
+
const isExpanded = !body.classList.contains('sidebar-hidden');
|
|
69
|
+
if (burgerMenu) {
|
|
70
|
+
if (isExpanded) {
|
|
71
|
+
burgerMenu.classList.add('active'); // Expanded, show X
|
|
72
|
+
} else {
|
|
73
|
+
burgerMenu.classList.remove('active'); // Collapsed to icons, show hamburger
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// In hide mode, burger active = sidebar visible
|
|
78
|
+
if (burgerMenu) {
|
|
79
|
+
if (body.classList.contains('sidebar-hidden')) {
|
|
80
|
+
burgerMenu.classList.remove('active'); // Hidden, show hamburger
|
|
81
|
+
} else {
|
|
82
|
+
burgerMenu.classList.add('active'); // Visible, show X
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Compact mode
|
|
89
|
+
const isCompactMode = localStorage.getItem('compact-mode') === 'true';
|
|
90
|
+
compactMode.checked = isCompactMode;
|
|
91
|
+
if (isCompactMode) {
|
|
92
|
+
body.classList.add('compact-mode');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Container width - read from body class
|
|
96
|
+
const containerWidthClasses = ['pa-container-sm', 'pa-container-md', 'pa-container-lg', 'pa-container-xl', 'pa-container-2xl'];
|
|
97
|
+
let currentContainerWidth = 'fluid';
|
|
98
|
+
for (const cls of containerWidthClasses) {
|
|
99
|
+
if (body.classList.contains(cls)) {
|
|
100
|
+
currentContainerWidth = cls.replace('pa-container-', '');
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
containerWidthSelector.value = currentContainerWidth;
|
|
105
|
+
|
|
106
|
+
// Sidebar mode - check if body has sticky class
|
|
107
|
+
const currentSidebarMode = body.classList.contains('pa-layout--sticky') ? 'sticky' : '';
|
|
108
|
+
sidebarModeSelector.value = currentSidebarMode;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Toggle panel
|
|
112
|
+
settingsToggle.addEventListener('click', () => {
|
|
113
|
+
settingsPanel.classList.toggle('pa-settings-panel--open');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Close panel when clicking outside
|
|
117
|
+
document.addEventListener('click', (e) => {
|
|
118
|
+
if (!settingsPanel.contains(e.target) && settingsPanel.classList.contains('pa-settings-panel--open')) {
|
|
119
|
+
settingsPanel.classList.remove('pa-settings-panel--open');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Theme change
|
|
124
|
+
themeSelector.addEventListener('change', (e) => {
|
|
125
|
+
const theme = e.target.value;
|
|
126
|
+
switchTheme(theme);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Font size change
|
|
130
|
+
fontSizeSelector.addEventListener('change', (e) => {
|
|
131
|
+
const size = e.target.value;
|
|
132
|
+
document.documentElement.classList.remove('font-size-small', 'font-size-default', 'font-size-large', 'font-size-xlarge');
|
|
133
|
+
if (size !== 'default') {
|
|
134
|
+
document.documentElement.classList.add(`font-size-${size}`);
|
|
135
|
+
}
|
|
136
|
+
localStorage.setItem('font-size', size);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Font family change
|
|
140
|
+
fontFamilySelector.addEventListener('change', (e) => {
|
|
141
|
+
const family = e.target.value;
|
|
142
|
+
body.classList.remove('font-family-serif', 'font-family-mono');
|
|
143
|
+
if (family !== 'default') {
|
|
144
|
+
body.classList.add(`font-family-${family}`);
|
|
145
|
+
}
|
|
146
|
+
localStorage.setItem('font-family', family);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Sidebar toggle
|
|
150
|
+
sidebarCollapsed.addEventListener('change', (e) => {
|
|
151
|
+
toggleSidebar();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Compact mode toggle
|
|
155
|
+
compactMode.addEventListener('change', (e) => {
|
|
156
|
+
if (e.target.checked) {
|
|
157
|
+
body.classList.add('compact-mode');
|
|
158
|
+
localStorage.setItem('compact-mode', 'true');
|
|
159
|
+
} else {
|
|
160
|
+
body.classList.remove('compact-mode');
|
|
161
|
+
localStorage.setItem('compact-mode', 'false');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Container width change - reload page with query param to set cookie
|
|
166
|
+
containerWidthSelector.addEventListener('change', (e) => {
|
|
167
|
+
const width = e.target.value;
|
|
168
|
+
const url = new URL(window.location);
|
|
169
|
+
url.searchParams.set('containerWidth', width);
|
|
170
|
+
window.location.href = url.toString();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Sidebar mode change
|
|
174
|
+
sidebarModeSelector.addEventListener('change', (e) => {
|
|
175
|
+
const mode = e.target.value;
|
|
176
|
+
switchSidebarMode(mode);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Sidebar behavior change
|
|
180
|
+
sidebarBehaviorSelector.addEventListener('change', (e) => {
|
|
181
|
+
const behavior = e.target.value;
|
|
182
|
+
const sidebar = document.querySelector('.pa-layout__sidebar');
|
|
183
|
+
const burgerMenu = document.querySelector('.burger-menu');
|
|
184
|
+
|
|
185
|
+
if (sidebar) {
|
|
186
|
+
sidebar.classList.remove('pa-layout__sidebar--icon-collapse');
|
|
187
|
+
document.body.classList.remove('sidebar-hidden');
|
|
188
|
+
|
|
189
|
+
if (behavior === 'icon-collapse') {
|
|
190
|
+
// Show icon-only sidebar in collapsed state
|
|
191
|
+
sidebar.classList.add('pa-layout__sidebar--icon-collapse');
|
|
192
|
+
// Start in collapsed state (icon bar showing)
|
|
193
|
+
if (burgerMenu) burgerMenu.classList.remove('active'); // Not expanded, show hamburger
|
|
194
|
+
localStorage.setItem('sidebar-hidden', 'false'); // Not fully hidden, just in icon mode
|
|
195
|
+
} else if (behavior === 'hide') {
|
|
196
|
+
// Hide sidebar completely
|
|
197
|
+
document.body.classList.add('sidebar-hidden');
|
|
198
|
+
if (burgerMenu) burgerMenu.classList.remove('active'); // Hidden, show hamburger to open
|
|
199
|
+
localStorage.setItem('sidebar-hidden', 'true');
|
|
200
|
+
} else {
|
|
201
|
+
// Default behavior - show full sidebar
|
|
202
|
+
if (burgerMenu) burgerMenu.classList.add('active'); // Visible, show X to close
|
|
203
|
+
localStorage.setItem('sidebar-hidden', 'false');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
localStorage.setItem('sidebar-behavior', behavior);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Reset settings
|
|
211
|
+
resetSettings.addEventListener('click', () => {
|
|
212
|
+
// Clear localStorage settings
|
|
213
|
+
localStorage.removeItem('font-size');
|
|
214
|
+
localStorage.removeItem('font-family');
|
|
215
|
+
localStorage.removeItem('sidebar-hidden');
|
|
216
|
+
localStorage.removeItem('sidebar-behavior');
|
|
217
|
+
localStorage.removeItem('compact-mode');
|
|
218
|
+
|
|
219
|
+
// Reset theme, container width, and sidebar mode to defaults
|
|
220
|
+
const url = new URL(window.location);
|
|
221
|
+
url.searchParams.set('theme', 'audi');
|
|
222
|
+
url.searchParams.set('containerWidth', 'fluid');
|
|
223
|
+
url.searchParams.set('sidebarMode', '');
|
|
224
|
+
window.location.href = url.toString();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Load settings on init
|
|
228
|
+
loadSettings();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Helper functions (need to be globally accessible for other scripts)
|
|
232
|
+
window.switchTheme = function(theme) {
|
|
233
|
+
// Redirect to current page with theme parameter
|
|
234
|
+
const url = new URL(window.location);
|
|
235
|
+
url.searchParams.set('theme', theme);
|
|
236
|
+
window.location.href = url.toString();
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
window.switchSidebarMode = function(mode) {
|
|
240
|
+
// Redirect to current page with sidebar mode parameter
|
|
241
|
+
const url = new URL(window.location);
|
|
242
|
+
url.searchParams.set('sidebarMode', mode);
|
|
243
|
+
window.location.href = url.toString();
|
|
244
|
+
};
|
|
245
|
+
})();
|