@ktfth/stickjs 3.0.0

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.
@@ -0,0 +1,422 @@
1
+ /*!
2
+ * Stick UI — Autocomplete plugin
3
+ * Usage: data-stick="input:autocomplete:#list-id"
4
+ * data-stick="input:autocomplete" data-stk-autocomplete-url="/api/search?q="
5
+ * API: stkAutocomplete.setOptions(inputEl, [{label, value}])
6
+ */
7
+ (function (root) {
8
+ 'use strict';
9
+
10
+ var uid = 0;
11
+ var instances = []; // track all active autocompletes for click-outside
12
+
13
+ /* ── Helpers ─────────────────────────────────────────── */
14
+
15
+ function escapeHTML(str) {
16
+ var div = document.createElement('div');
17
+ div.appendChild(document.createTextNode(str));
18
+ return div.innerHTML;
19
+ }
20
+
21
+ function highlightMatch(text, query) {
22
+ if (!query) return escapeHTML(text);
23
+ var idx = text.toLowerCase().indexOf(query.toLowerCase());
24
+ if (idx === -1) return escapeHTML(text);
25
+ return escapeHTML(text.substring(0, idx)) +
26
+ '<mark>' + escapeHTML(text.substring(idx, idx + query.length)) + '</mark>' +
27
+ escapeHTML(text.substring(idx + query.length));
28
+ }
29
+
30
+ function normalizeOption(item) {
31
+ if (typeof item === 'string') return { label: item, value: item };
32
+ return { label: item.label || item.value || '', value: item.value || item.label || '' };
33
+ }
34
+
35
+ function debounce(fn, ms) {
36
+ var timer;
37
+ return function () {
38
+ var ctx = this, args = arguments;
39
+ clearTimeout(timer);
40
+ timer = setTimeout(function () { fn.apply(ctx, args); }, ms);
41
+ };
42
+ }
43
+
44
+ /* ── Instance state ─────────────────────────────────── */
45
+
46
+ function getInstance(input) {
47
+ for (var i = 0; i < instances.length; i++) {
48
+ if (instances[i].input === input) return instances[i];
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function createInstance(input) {
54
+ var existing = getInstance(input);
55
+ if (existing) return existing;
56
+
57
+ var id = 'stk-ac-' + (++uid);
58
+
59
+ // Create dropdown
60
+ var dropdown = document.createElement('div');
61
+ dropdown.className = 'stk-autocomplete-dropdown';
62
+ dropdown.id = id + '-listbox';
63
+ dropdown.setAttribute('role', 'listbox');
64
+ dropdown.hidden = true;
65
+
66
+ // Insert dropdown after input (inside the .stk-autocomplete wrapper if present)
67
+ var wrapper = input.closest('.stk-autocomplete');
68
+ if (wrapper) {
69
+ wrapper.appendChild(dropdown);
70
+ } else {
71
+ input.parentNode.insertBefore(dropdown, input.nextSibling);
72
+ }
73
+
74
+ // ARIA on input
75
+ input.setAttribute('role', 'combobox');
76
+ input.setAttribute('aria-autocomplete', 'list');
77
+ input.setAttribute('aria-expanded', 'false');
78
+ input.setAttribute('aria-controls', dropdown.id);
79
+ input.setAttribute('autocomplete', 'off');
80
+
81
+ var inst = {
82
+ input: input,
83
+ dropdown: dropdown,
84
+ options: [], // [{label, value}]
85
+ filtered: [],
86
+ selectedIndex: -1,
87
+ remoteUrl: null,
88
+ minChars: 1,
89
+ fetching: false
90
+ };
91
+
92
+ instances.push(inst);
93
+ return inst;
94
+ }
95
+
96
+ /* ── Options loading ────────────────────────────────── */
97
+
98
+ function loadStaticOptions(inst, selector) {
99
+ var src = selector || inst.input.getAttribute('data-stk-autocomplete-src');
100
+ if (!src) return;
101
+
102
+ var el = document.querySelector(src);
103
+ if (!el) return;
104
+
105
+ var items = [];
106
+ if (el.tagName === 'DATALIST') {
107
+ var opts = el.querySelectorAll('option');
108
+ for (var i = 0; i < opts.length; i++) {
109
+ items.push({ label: opts[i].textContent || opts[i].value, value: opts[i].value || opts[i].textContent });
110
+ }
111
+ } else {
112
+ // <ul> or similar — grab <li> children
113
+ var lis = el.querySelectorAll('li');
114
+ for (var j = 0; j < lis.length; j++) {
115
+ var val = lis[j].getAttribute('data-value') || lis[j].textContent.trim();
116
+ items.push({ label: lis[j].textContent.trim(), value: val });
117
+ }
118
+ }
119
+
120
+ inst.options = items;
121
+ }
122
+
123
+ function fetchRemote(inst, query) {
124
+ if (inst.fetching) return;
125
+ inst.fetching = true;
126
+ showLoading(inst);
127
+
128
+ var url = inst.remoteUrl + encodeURIComponent(query);
129
+ var xhr = new XMLHttpRequest();
130
+ xhr.open('GET', url, true);
131
+ xhr.setRequestHeader('Accept', 'application/json');
132
+
133
+ xhr.onload = function () {
134
+ inst.fetching = false;
135
+ hideLoading(inst);
136
+ if (xhr.status >= 200 && xhr.status < 300) {
137
+ try {
138
+ var data = JSON.parse(xhr.responseText);
139
+ if (Array.isArray(data)) {
140
+ inst.options = data.map(normalizeOption);
141
+ filterAndRender(inst, query);
142
+ }
143
+ } catch (e) { /* ignore parse errors */ }
144
+ }
145
+ };
146
+
147
+ xhr.onerror = function () {
148
+ inst.fetching = false;
149
+ hideLoading(inst);
150
+ };
151
+
152
+ xhr.send();
153
+ }
154
+
155
+ /* ── Rendering ──────────────────────────────────────── */
156
+
157
+ function filterAndRender(inst, query) {
158
+ if (!query || query.length < inst.minChars) {
159
+ closeDropdown(inst);
160
+ return;
161
+ }
162
+
163
+ var q = query.toLowerCase();
164
+ inst.filtered = inst.options.filter(function (opt) {
165
+ return opt.label.toLowerCase().indexOf(q) !== -1;
166
+ });
167
+ inst.selectedIndex = -1;
168
+
169
+ renderItems(inst, query);
170
+
171
+ if (inst.filtered.length > 0 || query.length >= inst.minChars) {
172
+ openDropdown(inst);
173
+ }
174
+ }
175
+
176
+ function renderItems(inst, query) {
177
+ var dd = inst.dropdown;
178
+ dd.innerHTML = '';
179
+
180
+ if (inst.filtered.length === 0) {
181
+ var empty = document.createElement('div');
182
+ empty.className = 'stk-autocomplete-empty';
183
+ empty.textContent = 'No results found.';
184
+ dd.appendChild(empty);
185
+ return;
186
+ }
187
+
188
+ for (var i = 0; i < inst.filtered.length; i++) {
189
+ var opt = inst.filtered[i];
190
+ var item = document.createElement('div');
191
+ item.className = 'stk-autocomplete-item';
192
+ item.setAttribute('role', 'option');
193
+ item.setAttribute('aria-selected', 'false');
194
+ item.id = dd.id + '-opt-' + i;
195
+ item.dataset.index = String(i);
196
+ item.innerHTML = highlightMatch(opt.label, query);
197
+
198
+ // Click handler (closure)
199
+ item.addEventListener('mousedown', (function (idx) {
200
+ return function (e) {
201
+ e.preventDefault(); // prevent blur on input
202
+ selectOption(inst, idx);
203
+ };
204
+ })(i));
205
+
206
+ item.addEventListener('mouseenter', (function (idx) {
207
+ return function () {
208
+ setHighlight(inst, idx);
209
+ };
210
+ })(i));
211
+
212
+ dd.appendChild(item);
213
+ }
214
+ }
215
+
216
+ function showLoading(inst) {
217
+ var dd = inst.dropdown;
218
+ dd.innerHTML = '';
219
+ var loader = document.createElement('div');
220
+ loader.className = 'stk-autocomplete-loading';
221
+ loader.textContent = 'Loading\u2026';
222
+ dd.appendChild(loader);
223
+ openDropdown(inst);
224
+ }
225
+
226
+ function hideLoading(inst) {
227
+ var loader = inst.dropdown.querySelector('.stk-autocomplete-loading');
228
+ if (loader) loader.remove();
229
+ }
230
+
231
+ /* ── Dropdown open/close ────────────────────────────── */
232
+
233
+ function openDropdown(inst) {
234
+ inst.dropdown.hidden = false;
235
+ inst.input.setAttribute('aria-expanded', 'true');
236
+ }
237
+
238
+ function closeDropdown(inst) {
239
+ inst.dropdown.hidden = true;
240
+ inst.input.setAttribute('aria-expanded', 'false');
241
+ inst.selectedIndex = -1;
242
+ inst.input.removeAttribute('aria-activedescendant');
243
+ }
244
+
245
+ /* ── Highlight / select ─────────────────────────────── */
246
+
247
+ function setHighlight(inst, idx) {
248
+ inst.selectedIndex = idx;
249
+ var items = inst.dropdown.querySelectorAll('.stk-autocomplete-item');
250
+ for (var i = 0; i < items.length; i++) {
251
+ items[i].setAttribute('aria-selected', String(i === idx));
252
+ }
253
+ if (idx >= 0 && items[idx]) {
254
+ items[idx].scrollIntoView({ block: 'nearest' });
255
+ inst.input.setAttribute('aria-activedescendant', items[idx].id);
256
+ } else {
257
+ inst.input.removeAttribute('aria-activedescendant');
258
+ }
259
+ }
260
+
261
+ function selectOption(inst, idx) {
262
+ var opt = inst.filtered[idx];
263
+ if (!opt) return;
264
+ inst.input.value = opt.label;
265
+ closeDropdown(inst);
266
+
267
+ // Dispatch custom event
268
+ var ev;
269
+ try {
270
+ ev = new CustomEvent('autocomplete-selected', { bubbles: true, detail: { value: opt.value, label: opt.label } });
271
+ } catch (e) {
272
+ ev = document.createEvent('CustomEvent');
273
+ ev.initCustomEvent('autocomplete-selected', true, true, { value: opt.value, label: opt.label });
274
+ }
275
+ inst.input.dispatchEvent(ev);
276
+ }
277
+
278
+ /* ── Keyboard handler ───────────────────────────────── */
279
+
280
+ function onKeydown(inst, e) {
281
+ var count = inst.filtered.length;
282
+
283
+ if (e.key === 'ArrowDown') {
284
+ e.preventDefault();
285
+ if (!inst.dropdown.hidden && count) {
286
+ setHighlight(inst, (inst.selectedIndex + 1) % count);
287
+ } else if (inst.dropdown.hidden && inst.input.value.length >= inst.minChars) {
288
+ filterAndRender(inst, inst.input.value);
289
+ }
290
+ } else if (e.key === 'ArrowUp') {
291
+ e.preventDefault();
292
+ if (count) {
293
+ setHighlight(inst, (inst.selectedIndex - 1 + count) % count);
294
+ }
295
+ } else if (e.key === 'Enter') {
296
+ if (inst.selectedIndex >= 0 && !inst.dropdown.hidden) {
297
+ e.preventDefault();
298
+ selectOption(inst, inst.selectedIndex);
299
+ }
300
+ } else if (e.key === 'Escape') {
301
+ if (!inst.dropdown.hidden) {
302
+ e.preventDefault();
303
+ closeDropdown(inst);
304
+ }
305
+ } else if (e.key === 'Tab') {
306
+ if (inst.selectedIndex >= 0 && !inst.dropdown.hidden) {
307
+ selectOption(inst, inst.selectedIndex);
308
+ }
309
+ }
310
+ }
311
+
312
+ /* ── Click outside ──────────────────────────────────── */
313
+
314
+ document.addEventListener('click', function (e) {
315
+ for (var i = 0; i < instances.length; i++) {
316
+ var inst = instances[i];
317
+ if (!inst.dropdown.hidden &&
318
+ !inst.input.contains(e.target) &&
319
+ !inst.dropdown.contains(e.target)) {
320
+ closeDropdown(inst);
321
+ }
322
+ }
323
+ }, true);
324
+
325
+ /* ── Public API ──────────────────────────────────────── */
326
+
327
+ var stkAutocomplete = {
328
+ install: function (stick) {
329
+
330
+ /* autocomplete handler — called on input events */
331
+ stick.add('autocomplete', function (el, param, e) {
332
+ var inst = getInstance(el);
333
+
334
+ // First-time init
335
+ if (!inst) {
336
+ inst = createInstance(el);
337
+
338
+ // Determine data source
339
+ var remoteUrl = el.getAttribute('data-stk-autocomplete-url');
340
+ var minAttr = el.getAttribute('data-stk-autocomplete-min');
341
+ inst.minChars = minAttr ? parseInt(minAttr, 10) : 1;
342
+
343
+ if (remoteUrl) {
344
+ inst.remoteUrl = remoteUrl;
345
+ } else {
346
+ loadStaticOptions(inst, param || null);
347
+ }
348
+
349
+ // Attach keydown once
350
+ el.addEventListener('keydown', function (ke) {
351
+ onKeydown(inst, ke);
352
+ });
353
+
354
+ // Close on blur (with small delay so click on item registers)
355
+ el.addEventListener('blur', function () {
356
+ setTimeout(function () { closeDropdown(inst); }, 150);
357
+ });
358
+ }
359
+
360
+ // On each input event, filter
361
+ var query = el.value;
362
+ if (inst.remoteUrl) {
363
+ if (query.length >= inst.minChars) {
364
+ // Use debounced fetch
365
+ if (!inst._debouncedFetch) {
366
+ inst._debouncedFetch = debounce(function (q) {
367
+ fetchRemote(inst, q);
368
+ }, 300);
369
+ }
370
+ inst._debouncedFetch(query);
371
+ } else {
372
+ closeDropdown(inst);
373
+ }
374
+ } else {
375
+ filterAndRender(inst, query);
376
+ }
377
+ });
378
+
379
+ /* autocomplete-select handler — manually trigger selection */
380
+ stick.add('autocomplete-select', function (el, param, e, target) {
381
+ // Find the autocomplete input this belongs to
382
+ var input = target || el.closest('.stk-autocomplete');
383
+ if (input) input = input.querySelector('[role="combobox"]');
384
+ if (!input) return;
385
+
386
+ var inst = getInstance(input);
387
+ if (!inst) return;
388
+
389
+ var value = param || el.textContent.trim();
390
+ // Find matching option
391
+ for (var i = 0; i < inst.filtered.length; i++) {
392
+ if (inst.filtered[i].value === value || inst.filtered[i].label === value) {
393
+ selectOption(inst, i);
394
+ return;
395
+ }
396
+ }
397
+ // Fallback: set raw value
398
+ input.value = value;
399
+ closeDropdown(inst);
400
+ });
401
+ },
402
+
403
+ /**
404
+ * Set options programmatically for an input element.
405
+ * @param {HTMLInputElement} inputEl
406
+ * @param {Array<string|{label:string, value:string}>} options
407
+ */
408
+ setOptions: function (inputEl, options) {
409
+ var inst = getInstance(inputEl);
410
+ if (!inst) inst = createInstance(inputEl);
411
+ inst.options = (options || []).map(normalizeOption);
412
+ // If input has a value, re-filter
413
+ if (inputEl.value) {
414
+ filterAndRender(inst, inputEl.value);
415
+ }
416
+ }
417
+ };
418
+
419
+ if (root.Stick) root.Stick.use(stkAutocomplete);
420
+ if (typeof module !== 'undefined' && module.exports) module.exports = stkAutocomplete;
421
+ root.stkAutocomplete = stkAutocomplete;
422
+ }(typeof window !== 'undefined' ? window : this));
@@ -0,0 +1,289 @@
1
+ /*!
2
+ * Stick UI — Command Palette plugin
3
+ * Usage: data-stick="click:command-palette"
4
+ * Global shortcut: Ctrl+K (Cmd+K on Mac)
5
+ * API: stkCommandPalette.register([{ id, label, group?, shortcut?, action }])
6
+ */
7
+ (function (root) {
8
+ 'use strict';
9
+
10
+ var commands = [];
11
+ var overlay = null;
12
+ var input = null;
13
+ var list = null;
14
+ var selectedIndex = -1;
15
+ var filtered = [];
16
+
17
+ function buildDOM() {
18
+ if (overlay) return;
19
+
20
+ overlay = document.createElement('div');
21
+ overlay.className = 'stk-command-palette-overlay';
22
+ overlay.addEventListener('click', function (e) {
23
+ if (e.target === overlay) close();
24
+ });
25
+
26
+ var dialog = document.createElement('div');
27
+ dialog.className = 'stk-command-palette';
28
+ dialog.setAttribute('role', 'dialog');
29
+ dialog.setAttribute('aria-label', 'Command palette');
30
+
31
+ input = document.createElement('input');
32
+ input.className = 'stk-command-palette-input';
33
+ input.type = 'text';
34
+ input.placeholder = 'Type a command\u2026';
35
+ input.setAttribute('aria-autocomplete', 'list');
36
+ input.setAttribute('aria-controls', 'stk-command-palette-listbox');
37
+ input.addEventListener('input', onInput);
38
+ input.addEventListener('keydown', onKeydown);
39
+
40
+ list = document.createElement('div');
41
+ list.className = 'stk-command-palette-list';
42
+ list.id = 'stk-command-palette-listbox';
43
+ list.setAttribute('role', 'listbox');
44
+
45
+ dialog.appendChild(input);
46
+ dialog.appendChild(list);
47
+ overlay.appendChild(dialog);
48
+ document.body.appendChild(overlay);
49
+ }
50
+
51
+ function destroyDOM() {
52
+ if (overlay && overlay.parentElement) {
53
+ overlay.parentElement.removeChild(overlay);
54
+ }
55
+ overlay = null;
56
+ input = null;
57
+ list = null;
58
+ }
59
+
60
+ function isOpen() {
61
+ return overlay && !overlay.hidden;
62
+ }
63
+
64
+ function open() {
65
+ buildDOM();
66
+ overlay.hidden = false;
67
+ input.value = '';
68
+ selectedIndex = -1;
69
+ render('');
70
+ // Focus after paint so animation + focus work together
71
+ setTimeout(function () { input.focus(); }, 0);
72
+ }
73
+
74
+ function close() {
75
+ if (overlay) overlay.hidden = true;
76
+ }
77
+
78
+ function toggle() {
79
+ if (isOpen()) close(); else open();
80
+ }
81
+
82
+ /* ── Filtering ─────────────────────────────────────── */
83
+
84
+ function match(text, query) {
85
+ return text.toLowerCase().indexOf(query.toLowerCase()) !== -1;
86
+ }
87
+
88
+ function getFiltered(query) {
89
+ if (!query) return commands.slice();
90
+ return commands.filter(function (cmd) {
91
+ return match(cmd.label, query) || (cmd.group && match(cmd.group, query));
92
+ });
93
+ }
94
+
95
+ /* ── Rendering ─────────────────────────────────────── */
96
+
97
+ function render(query) {
98
+ filtered = getFiltered(query);
99
+ list.innerHTML = '';
100
+
101
+ if (filtered.length === 0) {
102
+ var empty = document.createElement('div');
103
+ empty.className = 'stk-command-palette-empty';
104
+ empty.textContent = 'No results found.';
105
+ list.appendChild(empty);
106
+ selectedIndex = -1;
107
+ return;
108
+ }
109
+
110
+ var currentGroup = null;
111
+ for (var i = 0; i < filtered.length; i++) {
112
+ var cmd = filtered[i];
113
+
114
+ // Group header
115
+ if (cmd.group && cmd.group !== currentGroup) {
116
+ currentGroup = cmd.group;
117
+ var groupEl = document.createElement('div');
118
+ groupEl.className = 'stk-command-palette-group';
119
+ groupEl.textContent = currentGroup;
120
+ list.appendChild(groupEl);
121
+ }
122
+
123
+ var item = document.createElement('div');
124
+ item.className = 'stk-command-palette-item';
125
+ item.setAttribute('role', 'option');
126
+ item.setAttribute('aria-selected', 'false');
127
+ item.dataset.index = String(i);
128
+
129
+ var label = document.createElement('span');
130
+ label.className = 'stk-command-palette-label';
131
+ label.textContent = cmd.label;
132
+ item.appendChild(label);
133
+
134
+ if (cmd.shortcut) {
135
+ var badge = document.createElement('span');
136
+ badge.className = 'stk-command-palette-shortcut';
137
+ badge.textContent = cmd.shortcut;
138
+ item.appendChild(badge);
139
+ }
140
+
141
+ item.addEventListener('click', (function (idx) {
142
+ return function () { execute(idx); };
143
+ })(i));
144
+
145
+ item.addEventListener('mouseenter', (function (idx) {
146
+ return function () { select(idx); };
147
+ })(i));
148
+
149
+ list.appendChild(item);
150
+ }
151
+
152
+ // Default select first item
153
+ if (filtered.length > 0) {
154
+ selectedIndex = 0;
155
+ updateSelection();
156
+ }
157
+ }
158
+
159
+ function updateSelection() {
160
+ var items = list.querySelectorAll('.stk-command-palette-item');
161
+ for (var i = 0; i < items.length; i++) {
162
+ var sel = (i === selectedIndex);
163
+ items[i].setAttribute('aria-selected', String(sel));
164
+ }
165
+ // Scroll selected into view
166
+ if (selectedIndex >= 0 && items[selectedIndex]) {
167
+ items[selectedIndex].scrollIntoView({ block: 'nearest' });
168
+ }
169
+ // Update aria-activedescendant
170
+ if (input && selectedIndex >= 0 && items[selectedIndex]) {
171
+ items[selectedIndex].id = 'stk-cp-opt-' + selectedIndex;
172
+ input.setAttribute('aria-activedescendant', items[selectedIndex].id);
173
+ }
174
+ }
175
+
176
+ function select(idx) {
177
+ selectedIndex = idx;
178
+ updateSelection();
179
+ }
180
+
181
+ function execute(idx) {
182
+ var cmd = filtered[idx];
183
+ if (cmd && typeof cmd.action === 'function') {
184
+ close();
185
+ cmd.action();
186
+ }
187
+ }
188
+
189
+ /* ── Input handlers ────────────────────────────────── */
190
+
191
+ function onInput() {
192
+ selectedIndex = -1;
193
+ render(input.value);
194
+ }
195
+
196
+ function onKeydown(e) {
197
+ var count = filtered.length;
198
+ if (e.key === 'ArrowDown') {
199
+ e.preventDefault();
200
+ if (count) {
201
+ selectedIndex = (selectedIndex + 1) % count;
202
+ updateSelection();
203
+ }
204
+ } else if (e.key === 'ArrowUp') {
205
+ e.preventDefault();
206
+ if (count) {
207
+ selectedIndex = (selectedIndex - 1 + count) % count;
208
+ updateSelection();
209
+ }
210
+ } else if (e.key === 'Enter') {
211
+ e.preventDefault();
212
+ if (selectedIndex >= 0) execute(selectedIndex);
213
+ } else if (e.key === 'Escape') {
214
+ e.preventDefault();
215
+ close();
216
+ }
217
+ }
218
+
219
+ /* ── Global shortcut Ctrl+K / Cmd+K ────────────────── */
220
+
221
+ document.addEventListener('keydown', function (e) {
222
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
223
+ e.preventDefault();
224
+ toggle();
225
+ }
226
+ // Also close on Escape if palette is open (handles focus outside input)
227
+ if (e.key === 'Escape' && isOpen()) {
228
+ close();
229
+ }
230
+ });
231
+
232
+ /* ── Public API ─────────────────────────────────────── */
233
+
234
+ var stkCommandPalette = {
235
+ install: function (stick) {
236
+ stick.add('command-palette', function () {
237
+ toggle();
238
+ });
239
+ },
240
+
241
+ /**
242
+ * Register commands.
243
+ * @param {Array<{id:string, label:string, group?:string, shortcut?:string, action:Function}>} cmds
244
+ */
245
+ register: function (cmds) {
246
+ if (!Array.isArray(cmds)) return;
247
+ for (var i = 0; i < cmds.length; i++) {
248
+ var cmd = cmds[i];
249
+ if (cmd && cmd.id && cmd.label) {
250
+ // Replace existing command with same id
251
+ var found = false;
252
+ for (var j = 0; j < commands.length; j++) {
253
+ if (commands[j].id === cmd.id) {
254
+ commands[j] = cmd;
255
+ found = true;
256
+ break;
257
+ }
258
+ }
259
+ if (!found) commands.push(cmd);
260
+ }
261
+ }
262
+ // Sort by group then label for consistent ordering
263
+ commands.sort(function (a, b) {
264
+ var ga = a.group || '';
265
+ var gb = b.group || '';
266
+ if (ga !== gb) return ga < gb ? -1 : 1;
267
+ return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
268
+ });
269
+ },
270
+
271
+ /** Open the palette programmatically. */
272
+ open: open,
273
+
274
+ /** Close the palette programmatically. */
275
+ close: close,
276
+
277
+ /** Toggle the palette. */
278
+ toggle: toggle,
279
+
280
+ /** Clear all registered commands. */
281
+ clear: function () {
282
+ commands = [];
283
+ }
284
+ };
285
+
286
+ if (root.Stick) root.Stick.use(stkCommandPalette);
287
+ if (typeof module !== 'undefined' && module.exports) module.exports = stkCommandPalette;
288
+ root.stkCommandPalette = stkCommandPalette;
289
+ }(typeof window !== 'undefined' ? window : this));