pixl-xyapp 2.1.7 → 2.1.9

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.md CHANGED
@@ -1,11 +1,28 @@
1
- # License
1
+ BSD 3-Clause License
2
2
 
3
- **The MIT License (MIT)**
3
+ Copyright (c) 2019 - 2026 PixlCore LLC & CONTRIBUTORS.
4
4
 
5
- *Copyright (c) 2025 PixlCore.com*
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
6
7
 
7
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
8
10
 
9
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
10
14
 
11
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/css/base.css CHANGED
@@ -1,5 +1,5 @@
1
1
  /* xyOps Base Theme */
2
- /* Copyright (c) 2019 Joseph Huckaby - MIT License */
2
+ /* Copyright (c) 2019 - 2026 Joseph Huckaby - BSD 3-Clause License */
3
3
 
4
4
  * {
5
5
  box-sizing: border-box;
@@ -178,6 +178,14 @@ body.dark fieldset input {
178
178
  background: var(--box-background-color);
179
179
  }
180
180
 
181
+ button.link {
182
+ background: none;
183
+ border: none;
184
+ padding: 0;
185
+ margin: 0;
186
+ font: inherit;
187
+ }
188
+
181
189
  a, .link {
182
190
  color: var(--link-color);
183
191
  }
@@ -197,6 +205,12 @@ a, .link {
197
205
  text-decoration: none;
198
206
  }
199
207
 
208
+ .link {
209
+ user-select: none;
210
+ -moz-user-select: none;
211
+ -webkit-user-select: none;
212
+ }
213
+
200
214
  a.danger:hover, .link.danger:hover {
201
215
  color: var(--red) !important;
202
216
  }
@@ -1490,9 +1504,6 @@ body.dark fieldset .checkbox_container > label:before {
1490
1504
  transform: scale(.2);
1491
1505
  }
1492
1506
 
1493
- /* .checkbox_container > input:hover ~ label:before {
1494
- border-color: var(--theme-color);
1495
- } */
1496
1507
  .checkbox_container > input:hover ~ label {
1497
1508
  color: var(--theme-color);
1498
1509
  }
@@ -1517,13 +1528,11 @@ body.dark fieldset .checkbox_container > label:before {
1517
1528
  .checkbox_container:hover {
1518
1529
  color: var(--body-text-color);
1519
1530
  }
1520
- /* .checkbox_container > input:hover ~ label:before {
1521
- background-color: var(--theme-color-half) !important;
1522
- border-color: var(--theme-color);
1523
- } */
1524
- /* .checkbox_container > input[checked]:hover ~ label:after {
1525
- background-color: var(--theme-color-half) !important;
1526
- } */
1531
+
1532
+ .checkbox_container > input:focus-visible ~ label::before {
1533
+ outline: 2px solid var(--theme-color);
1534
+ outline-offset: 2px;
1535
+ }
1527
1536
 
1528
1537
  div.info_label {
1529
1538
  font-size: 11px;
@@ -2398,6 +2407,7 @@ table.data_table.compact th:first-child, table.data_table.compact td:first-child
2398
2407
  }
2399
2408
 
2400
2409
  div.data_table_compact > div.data_grid > ul.grid_row_header > div {
2410
+ border-top: none;
2401
2411
  margin-bottom: 5px;
2402
2412
  }
2403
2413
 
package/js/base.js CHANGED
@@ -41,7 +41,7 @@ var app = {
41
41
 
42
42
  setHeaderNav: function(items) {
43
43
  // populate header with multiple nav elements
44
- var html = '<div class="header_nav_cont">';
44
+ var html = '<div class="header_nav_cont" role="navigation">';
45
45
 
46
46
  items.forEach( function(item, idx) {
47
47
  if (!item) return;
@@ -247,8 +247,11 @@ var app = {
247
247
  // show toast notification given raw html
248
248
  var { type, icon, msg, lifetime, loc } = args;
249
249
 
250
+ var role = 'alert';
251
+ if ((type == 'success') || (type == 'info')) role = 'status';
252
+
250
253
  var html = '';
251
- html += '<div class="toast ' + type + '" style="display:none">';
254
+ html += '<div class="toast ' + type + '" style="display:none" role="' + role + '" aria-atomic="true">';
252
255
  html += '<i class="mdi mdi-' + icon + '"></i>';
253
256
  html += '<span>' + msg + '</span>';
254
257
  html += '</div>';
@@ -536,6 +539,47 @@ var app = {
536
539
  notifyUserNav: function(loc) {
537
540
  // override in app
538
541
  // called by each page nav operation
542
+ },
543
+
544
+ focusNext($cur, $cont) {
545
+ // focus the next focusable element
546
+ if (!$cur) $cur = $(document.activeElement);
547
+ if (!$cont) $cont = $(document);
548
+
549
+ const $elems = $cont.find(
550
+ 'a[href], button, input, textarea, select, details, summary, [tabindex]:not([tabindex="-1"])'
551
+ ).filter(function() {
552
+ const $el = $(this);
553
+ if ($el.is(':disabled') || !$el.is(':visible')) return false;
554
+ if ($el.attr('tabindex') === "-1") return false;
555
+ return true;
556
+ });
557
+
558
+ // Find current element in list
559
+ const idx = $elems.index($cur);
560
+
561
+ // Focus the next tabbable element
562
+ if (idx !== -1 && idx < $elems.length - 1) {
563
+ const $next = $elems.eq(idx + 1);
564
+ $next.focus();
565
+ return $next;
566
+ }
567
+
568
+ return null;
569
+ },
570
+
571
+ buttonize: function($sel) {
572
+ // add aria roles and keyboard handlers to all buttons inside container
573
+ $sel.find('.button').attr({ role: 'button', tabindex: '0', onkeypress: 'app.buttonKey(this,event)' });
574
+ },
575
+
576
+ buttonKey: function(elem, event) {
577
+ // key pressed while button is focused -- click if space or enter
578
+ if ((event.key == 'Enter') || (event.key == ' ')) {
579
+ event.preventDefault();
580
+ event.stopPropagation();
581
+ $(elem).click();
582
+ }
539
583
  }
540
584
 
541
585
  }; // app object
package/js/datetime.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Joe's Date/Time Tools
2
- // Copyright (c) 2004 - 2025 Joseph Huckaby
3
- // Released under the MIT License
2
+ // Copyright (c) 2004 - 2026 Joseph Huckaby
3
+ // Released under the BSD 3-Clause License
4
4
 
5
5
  var _months = [
6
6
  [ 1, 'January' ], [ 2, 'February' ], [ 3, 'March' ], [ 4, 'April' ],
package/js/dialog.js CHANGED
@@ -45,7 +45,7 @@ var Dialog = {
45
45
  $('#dialog').stop().remove();
46
46
  }
47
47
 
48
- var $dialog = $('<div id="dialog" class="dialog"></div>').css({
48
+ var $dialog = $('<div id="dialog" class="dialog" role="dialog" aria-modal="true"></div>').css({
49
49
  opacity: 0,
50
50
  left: '' + x + 'px',
51
51
  top: '' + y + 'px'
@@ -56,6 +56,12 @@ var Dialog = {
56
56
 
57
57
  this.active = true;
58
58
  unscroll();
59
+
60
+ // buttonize all buttons
61
+ app.buttonize( $dialog );
62
+
63
+ // make everything behind us inert, for accessibility
64
+ $('div.sidebar, div.header, div.main, div.footer').attr('inert', 'true');
59
65
  },
60
66
 
61
67
  autoResize: function() {
@@ -83,6 +89,7 @@ var Dialog = {
83
89
  if (this.active) {
84
90
  $('#dialog').stop().fadeOut( 250, function() { $(this).remove(); } );
85
91
  $('#dialog_overlay').stop().fadeOut( 300, function() { $(this).remove(); } );
92
+ $('div.sidebar, div.header, div.main, div.footer').removeAttr('inert');
86
93
  this.active = false;
87
94
  unscroll.reset();
88
95
 
@@ -221,6 +228,9 @@ var Dialog = {
221
228
  Dialog.active = 'confirmation';
222
229
 
223
230
  setTimeout( function() {
231
+ // change aria role for danger type
232
+ $('#dialog').attr('role', 'alertdialog');
233
+
224
234
  // hold alt/opt key to immediately click default button
225
235
  if (app.lastClick.altKey) return Dialog.confirm_click(true);
226
236
  }, 1 );
@@ -300,7 +310,7 @@ var CodeEditor = {
300
310
  $('#ceditor').stop().remove();
301
311
  }
302
312
 
303
- var $dialog = $('<div class="dialog" id="ceditor"></div>').css({
313
+ var $dialog = $('<div class="dialog" id="ceditor" role="dialog" aria-modal="true"></div>').css({
304
314
  opacity: 0,
305
315
  left: '' + x + 'px',
306
316
  top: '' + y + 'px'
@@ -313,6 +323,17 @@ var CodeEditor = {
313
323
 
314
324
  // only do the unscroll thing if another dialog isn't active under us
315
325
  if (!Dialog.active) unscroll();
326
+
327
+ // if dialog is active under us, remove its modal role and make it inert
328
+ if (Dialog.active) {
329
+ $('#dialog').removeAttr('aria-modal').attr({ 'aria-hidden': 'true', 'inert': 'true' });
330
+ }
331
+ else {
332
+ $('div.sidebar, div.header, div.main, div.footer').attr('inert', 'true');
333
+ }
334
+
335
+ // buttonize all buttons
336
+ app.buttonize( $dialog );
316
337
  },
317
338
 
318
339
  autoResize: function() {
@@ -345,6 +366,14 @@ var CodeEditor = {
345
366
  // only release scroll lock if another dialog isn't active under us
346
367
  if (!Dialog.active) unscroll.reset();
347
368
 
369
+ // if dialog is active under us, restore its modal role
370
+ if (Dialog.active) {
371
+ $('#dialog').attr('aria-modal', 'true').removeAttr('aria-hidden').removeAttr('inert');
372
+ }
373
+ else {
374
+ $('div.sidebar, div.header, div.main, div.footer').removeAttr('inert');
375
+ }
376
+
348
377
  if (this.onHide) {
349
378
  // one time hook for hide
350
379
  var callback = this.onHide;
@@ -353,6 +382,7 @@ var CodeEditor = {
353
382
  }
354
383
 
355
384
  this.onDragDrop = null;
385
+ this.onKeyDown = null;
356
386
  }
357
387
  },
358
388
 
package/js/page.js CHANGED
@@ -88,7 +88,7 @@ window.Page = class Page {
88
88
 
89
89
  html += '<div class="form_row ' + extra_classes + '" ' + compose_attribs(args) + '>';
90
90
  if (label) html += '<div class="fr_label">' + label + '</div>';
91
- if (content) html += '<div class="fr_content">' + content + '</div>';
91
+ if (content) html += '<div class="fr_content" aria-label="' + strip_html(label).replace(/\&\w+\;/g, '').trim() + '">' + content + '</div>';
92
92
  if (suffix) html += '<div class="fr_suffix">' + suffix + '</div>';
93
93
  if (caption) html += '<div class="fr_caption"><span>' + inline_marked(caption) + '</span></div>'; // markdown
94
94
  html += '</div>';
@@ -161,7 +161,7 @@ window.Page = class Page {
161
161
 
162
162
  html += '<div class="checkbox_container">';
163
163
  html += '<input type="checkbox" ' + compose_attribs(args) + '/>';
164
- html += '<label>' + label + '</label>';
164
+ html += '<label for="' + (args.id || '') + '">' + label + '</label>';
165
165
  html += '</div>';
166
166
 
167
167
  return html;
@@ -1250,26 +1250,17 @@ window.Page = class Page {
1250
1250
  var $elem = $field.closest('.form_row').find('.fr_suffix .checker');
1251
1251
 
1252
1252
  if (username.match(/^[\w\-\.]+$/)) {
1253
- // check with server
1254
- app.api.get('app/check_user_exists', { username: username }, function(resp) {
1255
- if (!self.active) return; // sanity
1256
-
1257
- if (resp.user_exists) {
1258
- // username taken
1259
- $elem.css('color','red').html('<span class="mdi mdi-alert-circle"></span>').attr('title', "Username is taken.");
1260
- $field.addClass('warning');
1261
- }
1262
- else if (resp.user_invalid) {
1263
- // bad username
1264
- $elem.css('color', 'red').html('<span class="mdi mdi-alert-decagram"></span>').attr('title', "Username is malformed.");
1265
- $field.addClass('warning');
1266
- }
1267
- else {
1268
- // username is valid and available!
1269
- $elem.css('color','green').html('<span class="mdi mdi-check-circle"></span>').attr('title', "Username available!");
1270
- $field.removeClass('warning');
1271
- }
1272
- } );
1253
+ // check with user list
1254
+ if (find_object( app.users, { username })) {
1255
+ // username taken
1256
+ $elem.css('color','red').html('<span class="mdi mdi-alert-circle"></span>').attr('title', "Username is taken.");
1257
+ $field.addClass('warning');
1258
+ }
1259
+ else {
1260
+ // username is valid and available!
1261
+ $elem.css('color','green').html('<span class="mdi mdi-check-circle"></span>').attr('title', "Username available!");
1262
+ $field.removeClass('warning');
1263
+ }
1273
1264
  }
1274
1265
  else if (username.length) {
1275
1266
  // bad username
@@ -1726,10 +1717,6 @@ window.PageManager = class PageManager {
1726
1717
  var result = page.onActivate.apply(page, [args]);
1727
1718
  if (typeof(result) == 'boolean') return result;
1728
1719
  else throw("Page " + id + " onActivate did not return a boolean!");
1729
-
1730
- // expand section if applicable -- TODO: unreachable code:
1731
- var $sect = $('#tab_'+id).parent().prev();
1732
- if ($sect.length && $sect.hasClass('section_title')) this.expandSidebarGroup( $sect );
1733
1720
  }
1734
1721
 
1735
1722
  deactivate(id, new_id) {
package/js/popover.js CHANGED
@@ -68,6 +68,9 @@ var Popover = {
68
68
  }
69
69
 
70
70
  if (!Dialog.active) unscroll();
71
+
72
+ // buttonize all buttons
73
+ app.buttonize( $box );
71
74
  }, 1 );
72
75
  },
73
76
 
package/js/select.js CHANGED
@@ -9,9 +9,9 @@ var SingleSelect = {
9
9
  $(sel).each( function() {
10
10
  var self = this;
11
11
  var $this = $(this);
12
- $this.css('display', 'none');
12
+ $this.css('display', 'none').attr({ 'aria-hidden': true, tabindex: '-1' });
13
13
 
14
- var $ms = $('<div class="multiselect single"></div>');
14
+ var $ms = $('<div class="multiselect single" role="button" tabindex="0" aria-haspopup="listbox" aria-expanded="false"></div>');
15
15
  if ($this.data('private')) $ms.attr('data-private', 1);
16
16
  $this.after( $ms );
17
17
 
@@ -50,16 +50,25 @@ var SingleSelect = {
50
50
  // also expose redraw as a custom event that can be triggered
51
51
  $this.on('redraw', redraw);
52
52
 
53
- $ms.on('mouseup', function() {
53
+ // allow keyboard to open menu
54
+ $ms.on('keydown', function(event) {
55
+ if ((event.key == 'Enter') || (event.key == ' ')) {
56
+ $ms.click();
57
+ event.preventDefault();
58
+ }
59
+ } );
60
+
61
+ $ms.on('click', function() {
54
62
  // create popover dialog for selecting and filtering
55
63
  var html = '';
56
64
  var is_private = $this.data('private');
57
65
  if ($ms.hasClass('disabled')) return;
66
+ $ms.attr('aria-expanded', 'true');
58
67
 
59
68
  html += '<div class="sel_dialog_label">' + ($this.attr('title') || 'Select Item') + '</div>';
60
69
 
61
70
  if (!$this.data('compact')) {
62
- html += '<div class="sel_dialog_search_container">';
71
+ html += '<div class="sel_dialog_search_container" role="search">';
63
72
  html += '<input type="text" id="fe_sel_dialog_search" class="sel_dialog_search" autocomplete="off" value=""/>';
64
73
  html += '<div class="sel_dialog_search_icon"><i class="mdi mdi-magnify"></i></div>';
65
74
  html += '</div>';
@@ -67,7 +76,7 @@ var SingleSelect = {
67
76
 
68
77
  html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea';
69
78
  if ($this.data('nudgeheight')) html += ' nudgeheight';
70
- html += '">';
79
+ html += '" role="listbox">';
71
80
 
72
81
  for (var idx = 0, len = self.options.length; idx < len; idx++) {
73
82
  var opt = self.options[idx];
@@ -83,7 +92,8 @@ var SingleSelect = {
83
92
  }
84
93
  if ($this.data('shrinkwrap')) html += ' shrinkwrap';
85
94
 
86
- html += '" ' + ((idx >= SingleSelect.maxMenuItems) ? 'style="display:none"' : '') + ' data-value="' + opt.value + '">';
95
+ html += '" ' + ((idx >= SingleSelect.maxMenuItems) ? 'style="display:none"' : '') + ' data-value="' + opt.value + '"';
96
+ html += ' role="option" aria-selected="' + (opt.selected ? 'true' : 'false') + '" tabindex="-1">';
87
97
 
88
98
  if (opt.getAttribute && opt.getAttribute('data-icon')) {
89
99
  html += '<i class="mdi mdi-' + opt.getAttribute('data-icon') + '">&nbsp;</i>';
@@ -100,11 +110,11 @@ var SingleSelect = {
100
110
  }
101
111
  html += '</div>';
102
112
 
103
- html += '</div>';
113
+ html += '</div>'; // scrollarea
104
114
 
105
115
  Popover.attach( $ms, '<div style="padding:15px;">' + html + '</div>', $this.data('shrinkwrap') || false );
106
116
 
107
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('mouseup', function() {
117
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('click', function() {
108
118
  // select item, close dialog and update multi-select
109
119
  var $item = $(this);
110
120
  $this.val( $item.data('value') );
@@ -154,19 +164,64 @@ var SingleSelect = {
154
164
  if ((event.keyCode == 13) && value.length) {
155
165
  event.preventDefault();
156
166
  event.stopPropagation();
157
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item.match').slice(0, 1).trigger('mouseup');
167
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item.match').slice(0, 1).trigger('click');
158
168
  }
159
169
  });
160
170
  }
161
171
 
172
+ // handle keyboard focus
173
+ Popover.onKeyDown = SingleSelect.handleKeyDown;
174
+
162
175
  // highlight select field under us
163
176
  $ms.addClass('selected');
164
- Popover.onDetach = function() { $ms.removeClass('selected'); };
165
- }); // mouseup
177
+ Popover.onDetach = function() {
178
+ $ms.removeClass('selected').attr('aria-expanded', 'false').focus();
179
+ };
180
+ }); // click
166
181
 
167
182
  }); // forach elem
168
183
  },
169
184
 
185
+ handleKeyDown: function(event) {
186
+ var $options = $('#d_sel_dialog_scrollarea > div.sel_dialog_item');
187
+ var $active = $(document.activeElement).closest('.sel_dialog_item');
188
+
189
+ var focusItem = function($item) {
190
+ if (!$item.length) return;
191
+ $options.attr({ 'tabindex':'-1', 'aria-selected':'false' });
192
+ $item.attr({ 'tabindex':'0', 'aria-selected':'true' }).focus();
193
+ };
194
+
195
+ switch (event.key) {
196
+ case 'ArrowDown':
197
+ focusItem( $options.eq( $active.length ? ($active.index('.sel_dialog_item') + 1) : 0 ) );
198
+ event.preventDefault();
199
+ event.stopPropagation();
200
+ break;
201
+
202
+ case 'ArrowUp':
203
+ focusItem( $options.eq( $active.length ? Math.max(0, $active.index('.sel_dialog_item') - 1) : 0 ) );
204
+ event.preventDefault();
205
+ event.stopPropagation();
206
+ break;
207
+
208
+ case 'Enter':
209
+ case ' ':
210
+ if ($active.length) {
211
+ $active.click();
212
+ event.preventDefault();
213
+ event.stopPropagation();
214
+ }
215
+ break;
216
+
217
+ case 'Escape':
218
+ Popover.detach();
219
+ event.preventDefault();
220
+ event.stopPropagation();
221
+ break;
222
+ } // switch key
223
+ }, // onKeyDown
224
+
170
225
  popupQuickMenu: function(opts) {
171
226
  // show popup menu on custom element
172
227
  // opts: { elem, title, items, value, callback, nocheck?, onCancel? }
@@ -179,17 +234,20 @@ var SingleSelect = {
179
234
 
180
235
  html += '<div class="sel_dialog_label">' + opts.title + '</div>';
181
236
  if (opts.search) {
182
- html += '<div class="sel_dialog_search_container">';
237
+ html += '<div class="sel_dialog_search_container" role="search">';
183
238
  html += '<input type="text" id="fe_sel_dialog_search" class="sel_dialog_search" autocomplete="off" value=""/>';
184
239
  html += '<div class="sel_dialog_search_icon"><i class="mdi mdi-magnify"></i></div>';
185
240
  html += '</div>';
186
241
  }
187
- html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea">';
242
+ html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea" role="listbox">';
188
243
  for (var idy = 0, ley = items.length; idy < ley; idy++) {
189
244
  var item = items[idy];
190
245
  var sel = (item.id == opts.value);
191
246
  if (item.group) html += '<div class="sel_dialog_group">' + item.group + '</div>';
192
- html += '<div class="sel_dialog_item ' + check + ' ' + (sel ? 'selected' : '') + ' shrinkwrap" data-value="' + item.id + '">';
247
+
248
+ html += '<div class="sel_dialog_item ' + check + ' ' + (sel ? 'selected' : '') + ' shrinkwrap" data-value="' + item.id + '"';
249
+ html += ' role="option" aria-selected="' + (sel ? 'true' : 'false') + '" tabindex="-1">';
250
+
193
251
  if (item.icon) html += '<i class="mdi mdi-' + item.icon + '">&nbsp;</i>';
194
252
  html += '<span>' + item.title + '</span>';
195
253
  if (!opts.nocheck) html += '<div class="sel_dialog_item_check"><i class="mdi mdi-check"></i></div>';
@@ -199,7 +257,7 @@ var SingleSelect = {
199
257
 
200
258
  Popover.attach( $elem, '<div style="padding:15px;">' + html + '</div>', true );
201
259
 
202
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('mouseup', function() {
260
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('click', function() {
203
261
  // select item, close dialog and update state
204
262
  var $item = $(this);
205
263
  var value = $item.data('value');
@@ -207,10 +265,13 @@ var SingleSelect = {
207
265
  delete opts.onCancel;
208
266
  Popover.detach();
209
267
  callback(value);
210
- }); // mouseup
268
+ }); // click
269
+
270
+ // handle keyboard focus
271
+ Popover.onKeyDown = SingleSelect.handleKeyDown;
211
272
 
212
273
  Popover.onDetach = function() {
213
- $elem.removeClass('popped');
274
+ $elem.removeClass('popped').focus();
214
275
  if (opts.onCancel) opts.onCancel();
215
276
  };
216
277
 
@@ -249,7 +310,7 @@ var SingleSelect = {
249
310
  event.preventDefault();
250
311
  event.stopPropagation();
251
312
 
252
- var mup = jQuery.Event( "mouseup" );
313
+ var mup = jQuery.Event( "click" );
253
314
  mup.metaKey = true; // bypass `hold` feature
254
315
  $('#d_sel_dialog_scrollarea > div.sel_dialog_item.match').slice(0, 1).trigger(mup);
255
316
  }
@@ -266,9 +327,9 @@ var MultiSelect = {
266
327
  $(sel).each( function() {
267
328
  var self = this;
268
329
  var $this = $(this);
269
- $this.css('display', 'none');
330
+ $this.css('display', 'none').attr({ 'aria-hidden': true, 'tabindex': '-1' });
270
331
 
271
- var $ms = $('<div class="multiselect multi"></div>');
332
+ var $ms = $('<div class="multiselect multi" role="button" tabindex="0" aria-haspopup="listbox" aria-expanded="false"></div>');
272
333
  if ($this.data('compact')) $ms.addClass('compact');
273
334
  if ($this.data('private')) $ms.attr('data-private', 1);
274
335
  $this.after( $ms );
@@ -311,7 +372,7 @@ var MultiSelect = {
311
372
  if (num_sel) $ms.append( '<div class="clear"></div>' );
312
373
  else $ms.append( '<div class="placeholder">' + ($this.attr('placeholder') || 'Click to select...') + '</div>' );
313
374
 
314
- $ms.find('div.item > i.mdi-close').on('mouseup', function(e) {
375
+ $ms.find('div.item > i.mdi-close').on('click', function(e) {
315
376
  // user clicked on the 'X' -- remove this item and redraw
316
377
  var $item = $(this).parent();
317
378
  var value = $item.data('value');
@@ -335,8 +396,8 @@ var MultiSelect = {
335
396
 
336
397
  if ($this.data('hold') && $this.data('volatile') && Popover.enabled && (Popover.$anchor.prop('id') == $this.prop('id'))) {
337
398
  $('#d_sel_dialog_scrollarea > div.sel_dialog_item').each( function(idx) {
338
- if (self.options[idx].selected) $(this).addClass('selected');
339
- else $(this).removeClass('selected');
399
+ if (self.options[idx].selected) $(this).addClass('selected').attr('aria-selected', 'true');
400
+ else $(this).removeClass('selected').attr('aria-selected', 'false');
340
401
  } );
341
402
  }
342
403
  }; // redraw
@@ -349,7 +410,15 @@ var MultiSelect = {
349
410
  // also expose redraw as a custom event that can be triggered
350
411
  $this.on('redraw', redraw);
351
412
 
352
- $ms.on('mouseup', function() {
413
+ // allow keyboard to open menu
414
+ $ms.on('keydown', function(event) {
415
+ if ((event.key == 'Enter') || (event.key == ' ')) {
416
+ $ms.click();
417
+ event.preventDefault();
418
+ }
419
+ } );
420
+
421
+ $ms.on('click', function() {
353
422
  // create popover dialog for selecting and filtering
354
423
  var html = '';
355
424
  var orig_sel_state = [];
@@ -357,6 +426,7 @@ var MultiSelect = {
357
426
  var inherited = $this.data('inherited') ? ($this.data('inherited') || '').split(/\,\s*/) : [];
358
427
  var is_private = $this.data('private');
359
428
  if ($ms.hasClass('disabled')) return;
429
+ $ms.attr('aria-expanded', 'true');
360
430
 
361
431
  html += '<div class="sel_dialog_label">' + ($this.attr('title') || 'Select Items');
362
432
  if ($this.data('select-all')) {
@@ -365,11 +435,11 @@ var MultiSelect = {
365
435
  }
366
436
  html += '</div>';
367
437
 
368
- html += '<div class="sel_dialog_search_container">';
438
+ html += '<div class="sel_dialog_search_container" role="search">';
369
439
  html += '<input type="text" id="fe_sel_dialog_search" class="sel_dialog_search" autocomplete="off" value=""/>';
370
440
  html += '<div class="sel_dialog_search_icon"><i class="mdi mdi-magnify"></i></div>';
371
441
  html += '</div>';
372
- html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea">';
442
+ html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea" role="listbox">';
373
443
 
374
444
  for (var idx = 0, len = self.options.length; idx < len; idx++) {
375
445
  var opt = self.options[idx];
@@ -387,7 +457,9 @@ var MultiSelect = {
387
457
  if ($this.data('shrinkwrap')) html += ' shrinkwrap';
388
458
  if (is_inherited) html += ' inherited';
389
459
 
390
- html += '" data-value="' + opt.value + '" title="' + (is_inherited ? ($this.data('itooltip') || '(Inherited Item)') : '') + '">';
460
+ html += '" data-value="' + opt.value + '" title="' + (is_inherited ? ($this.data('itooltip') || '(Inherited Item)') : '') + '"';
461
+ html += ' role="option" aria-selected="' + (opt.selected ? 'true' : 'false') + '" tabindex="-1">';
462
+
391
463
  if (opt.getAttribute && opt.getAttribute('data-icon')) {
392
464
  html += '<i class="mdi mdi-' + opt.getAttribute('data-icon') + '">&nbsp;</i>';
393
465
  }
@@ -409,7 +481,7 @@ var MultiSelect = {
409
481
 
410
482
  Popover.attach( $ms, '<div style="padding:15px;">' + html + '</div>', $this.data('shrinkwrap') || false );
411
483
 
412
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('mouseup', function(event) {
484
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('click', function(event) {
413
485
  // select item, close dialog and update multi-select
414
486
  var $item = $(this);
415
487
  if ($item.hasClass('inherited')) return; // no clicky on inherited items
@@ -422,7 +494,7 @@ var MultiSelect = {
422
494
  var opt = self.options[idx];
423
495
  if (opt.value == value) {
424
496
  opt.selected = new_sel_state;
425
- if (new_sel_state) $item.addClass('selected'); else $item.removeClass('selected');
497
+ if (new_sel_state) $item.addClass('selected').attr('aria-selected', 'true'); else $item.removeClass('selected').attr('aria-selected', 'false');
426
498
  if (new_sel_state) new_sel_idx = idx;
427
499
  idx = len;
428
500
  }
@@ -436,7 +508,7 @@ var MultiSelect = {
436
508
  // select range
437
509
  if (last_sel_idx > new_sel_idx) { var temp = last_sel_idx; last_sel_idx = new_sel_idx; new_sel_idx = temp; }
438
510
  for (var idx = last_sel_idx; idx <= new_sel_idx; idx++) { self.options[idx].selected = true; }
439
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item').slice(last_sel_idx, new_sel_idx + 1).addClass('selected');
511
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item').slice(last_sel_idx, new_sel_idx + 1).addClass('selected').attr('aria-selected', 'true');
440
512
  }
441
513
  else if ($this.data('hold') && event.altKey && (new_sel_idx > -1) && (last_sel_idx > -1) && (new_sel_idx != last_sel_idx)) {
442
514
  // select pattern
@@ -445,7 +517,7 @@ var MultiSelect = {
445
517
  for (var idx = last_sel_idx, len = self.options.length; idx < len; idx += dist) {
446
518
  var opt = self.options[idx];
447
519
  opt.selected = true;
448
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item').slice(idx, idx + 1).addClass('selected');
520
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item').slice(idx, idx + 1).addClass('selected').attr('aria-selected', 'true');
449
521
  }
450
522
  }
451
523
  else if ($this.data('hold') && !new_sel_state) {
@@ -461,10 +533,10 @@ var MultiSelect = {
461
533
  var is_all_sel = !!(find_objects(self.options, { selected: true }).length == self.options.length);
462
534
  $('div.arrow_box div.sel_all_none').html( is_all_sel ? 'Select None' : 'Select All' );
463
535
  }
464
- }); // mouseup
536
+ }); // click
465
537
 
466
538
  if ($this.data('hold')) {
467
- $('#btn_sel_dialog_cancel').on('mouseup', function() {
539
+ $('#btn_sel_dialog_cancel').on('click', function() {
468
540
  Popover.detach();
469
541
 
470
542
  // restore original opts and redraw
@@ -476,11 +548,11 @@ var MultiSelect = {
476
548
  redraw();
477
549
  $this.trigger('change');
478
550
  });
479
- $('#btn_sel_dialog_add').on('mouseup', function() { Popover.detach(); });
551
+ $('#btn_sel_dialog_add').on('click', function() { Popover.detach(); $ms.focus(); });
480
552
  } // hold
481
553
 
482
554
  // attach click handler for select-all-none
483
- $('div.arrow_box div.sel_all_none').on('mouseup', function(event) {
555
+ $('div.arrow_box div.sel_all_none').on('click', function(event) {
484
556
  var is_all_sel = !!(find_objects(self.options, { selected: true }).length == self.options.length);
485
557
  var new_sel_state = !is_all_sel;
486
558
 
@@ -490,7 +562,7 @@ var MultiSelect = {
490
562
 
491
563
  var opt = self.options[idx];
492
564
  opt.selected = new_sel_state;
493
- if (new_sel_state) $item.addClass('selected'); else $item.removeClass('selected');
565
+ if (new_sel_state) $item.addClass('selected').attr('aria-selected', 'true'); else $item.removeClass('selected').attr('aria-selected', 'false');
494
566
  } );
495
567
 
496
568
  $this.trigger('change');
@@ -535,7 +607,7 @@ var MultiSelect = {
535
607
  event.preventDefault();
536
608
  event.stopPropagation();
537
609
 
538
- var mup = jQuery.Event( "mouseup" );
610
+ var mup = jQuery.Event( "click" );
539
611
  mup.metaKey = true; // bypass `hold` feature
540
612
  $('#d_sel_dialog_scrollarea > div.sel_dialog_item.match').slice(0, 1).trigger(mup);
541
613
  }
@@ -543,13 +615,13 @@ var MultiSelect = {
543
615
  // enter key WITHOUT value typed into search box + hold mode
544
616
  event.preventDefault();
545
617
  event.stopPropagation();
546
- $('#btn_sel_dialog_add').trigger( jQuery.Event("mouseup") );
618
+ $('#btn_sel_dialog_add').trigger( jQuery.Event("click") );
547
619
  }
548
620
  else if ((event.keyCode == 27) && $this.data('hold')) {
549
621
  // esc key WITHOUT value typed into search box + hold mode
550
622
  event.preventDefault();
551
623
  event.stopPropagation();
552
- $('#btn_sel_dialog_cancel').trigger( jQuery.Event("mouseup") );
624
+ $('#btn_sel_dialog_cancel').trigger( jQuery.Event("click") );
553
625
  }
554
626
  });
555
627
 
@@ -559,23 +631,26 @@ var MultiSelect = {
559
631
  // enter key
560
632
  event.preventDefault();
561
633
  event.stopPropagation();
562
- $('#btn_sel_dialog_add').trigger( jQuery.Event("mouseup") );
634
+ $('#btn_sel_dialog_add').trigger( jQuery.Event("click") );
563
635
  }
564
636
  else if ((event.keyCode == 27) && !$input.is(':focus') && $this.data('hold')) {
565
637
  // esc key
566
638
  event.preventDefault();
567
639
  event.stopPropagation();
568
- $('#btn_sel_dialog_cancel').trigger( jQuery.Event("mouseup") );
640
+ $('#btn_sel_dialog_cancel').trigger( jQuery.Event("click") );
569
641
  }
570
642
  };
571
643
 
644
+ // handle keyboard focus
645
+ Popover.onKeyDown = SingleSelect.handleKeyDown;
646
+
572
647
  // highlight multiselect field under us
573
648
  $ms.addClass('selected');
574
649
  Popover.onDetach = function() {
575
- $ms.removeClass('selected');
650
+ $ms.removeClass('selected').attr('aria-expanded', 'false').focus();
576
651
  if (Dialog.active) Dialog.autoResize();
577
652
  };
578
- }); // mouseup
653
+ }); // click
579
654
 
580
655
  }); // foreach elem
581
656
  },
@@ -591,16 +666,19 @@ var MultiSelect = {
591
666
 
592
667
  html += '<div class="sel_dialog_label">' + opts.title + '</div>';
593
668
  if (opts.search) {
594
- html += '<div class="sel_dialog_search_container">';
669
+ html += '<div class="sel_dialog_search_container" role="search">';
595
670
  html += '<input type="text" id="fe_sel_dialog_search" class="sel_dialog_search" autocomplete="off" value=""/>';
596
671
  html += '<div class="sel_dialog_search_icon"><i class="mdi mdi-magnify"></i></div>';
597
672
  html += '</div>';
598
673
  }
599
- html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea">';
674
+ html += '<div id="d_sel_dialog_scrollarea" class="sel_dialog_scrollarea" role="listbox">';
600
675
  for (var idy = 0, ley = items.length; idy < ley; idy++) {
601
676
  var item = items[idy];
602
677
  var sel = opts.values.includes(item.id);
603
- html += '<div class="sel_dialog_item check ' + (sel ? 'selected' : '') + ' shrinkwrap" data-value="' + item.id + '">';
678
+
679
+ html += '<div class="sel_dialog_item check ' + (sel ? 'selected' : '') + ' shrinkwrap" data-value="' + item.id + '"';
680
+ html += ' role="option" aria-selected="' + (sel ? 'true' : 'false') + '" tabindex="-1">';
681
+
604
682
  if (item.icon) html += '<i class="mdi mdi-' + item.icon + '">&nbsp;</i>';
605
683
  html += '<span>' + item.title + '</span>';
606
684
  html += '<div class="sel_dialog_item_check"><i class="mdi mdi-check"></i></div>';
@@ -610,7 +688,7 @@ var MultiSelect = {
610
688
 
611
689
  Popover.attach( $elem, '<div style="padding:15px;">' + html + '</div>', true );
612
690
 
613
- $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('mouseup', function() {
691
+ $('#d_sel_dialog_scrollarea > div.sel_dialog_item').on('click', function() {
614
692
  // select item, close dialog and update state
615
693
  var $item = $(this);
616
694
  if ($item.hasClass('selected')) $item.removeClass('selected');
@@ -623,10 +701,13 @@ var MultiSelect = {
623
701
 
624
702
  Popover.detach();
625
703
  callback(values);
626
- }); // mouseup
704
+ }); // click
705
+
706
+ // handle keyboard focus
707
+ Popover.onKeyDown = SingleSelect.handleKeyDown;
627
708
 
628
709
  Popover.onDetach = function() {
629
- $elem.removeClass('popped');
710
+ $elem.removeClass('popped').focus();
630
711
  };
631
712
 
632
713
  $elem.addClass('popped');
@@ -664,7 +745,7 @@ var MultiSelect = {
664
745
  event.preventDefault();
665
746
  event.stopPropagation();
666
747
 
667
- var mup = jQuery.Event( "mouseup" );
748
+ var mup = jQuery.Event( "click" );
668
749
  mup.metaKey = true; // bypass `hold` feature
669
750
  $('#d_sel_dialog_scrollarea > div.sel_dialog_item.match').slice(0, 1).trigger(mup);
670
751
  }
@@ -681,8 +762,9 @@ var TextSelect = {
681
762
  $(sel).each( function() {
682
763
  var self = this;
683
764
  var $this = $(this);
765
+ $this.css('display', 'none').attr({ 'aria-hidden': true, 'tabindex': '-1' });
684
766
 
685
- var $ms = $('<div class="multiselect text"></div>');
767
+ var $ms = $('<div class="multiselect text" role="button" tabindex="0"></div>');
686
768
  $this.after( $ms );
687
769
 
688
770
  var redraw = function() {
@@ -703,7 +785,7 @@ var TextSelect = {
703
785
  if (num_sel) $ms.append( '<div class="clear"></div>' );
704
786
  else $ms.append( '<div class="placeholder">' + ($this.attr('placeholder') || 'Click to add...') + '</div>' );
705
787
 
706
- $ms.find('div.item > i').on('mouseup', function(e) {
788
+ $ms.find('div.item > i').on('click', function(e) {
707
789
  // user clicked on the 'X' -- remove this item and redraw
708
790
  var $item = $(this).parent();
709
791
  var value = $item.data('value');
@@ -726,7 +808,15 @@ var TextSelect = {
726
808
  // also expose redraw as a custom event that can be triggered
727
809
  $this.on('redraw', redraw);
728
810
 
729
- $ms.on('mouseup', function() {
811
+ // allow keyboard to open menu
812
+ $ms.on('keydown', function(event) {
813
+ if ((event.key == 'Enter') || (event.key == ' ')) {
814
+ $ms.click();
815
+ event.preventDefault();
816
+ }
817
+ } );
818
+
819
+ $ms.on('click', function() {
730
820
  // create popover dialog for adding new items
731
821
  var html = '';
732
822
  if ($ms.hasClass('disabled')) return;
@@ -772,8 +862,8 @@ var TextSelect = {
772
862
  $this.trigger('change');
773
863
  }; // doAdd
774
864
 
775
- $('#btn_sel_dialog_cancel').on('mouseup', function() { Popover.detach(); });
776
- $('#btn_sel_dialog_add').on('mouseup', function() { doAdd(); });
865
+ $('#btn_sel_dialog_cancel').on('click', function() { Popover.detach(); });
866
+ $('#btn_sel_dialog_add').on('click', function() { doAdd(); });
777
867
 
778
868
  var $input = $('#fe_sel_dialog_text').focus().on('keydown', function(event) {
779
869
  // capture enter key
@@ -786,8 +876,10 @@ var TextSelect = {
786
876
 
787
877
  // highlight multiselect field under us
788
878
  $ms.addClass('selected');
789
- Popover.onDetach = function() { $ms.removeClass('selected'); };
790
- }); // mouseup
879
+ Popover.onDetach = function() {
880
+ $ms.removeClass('selected').focus();
881
+ };
882
+ }); // click
791
883
 
792
884
  }); // forach elem
793
885
  },
@@ -835,8 +927,8 @@ var TextSelect = {
835
927
  callback(value);
836
928
  }; // doAdd
837
929
 
838
- $('#btn_sel_dialog_cancel').on('mouseup', function() { Popover.detach(); });
839
- $('#btn_sel_dialog_add').on('mouseup', function() { doAdd(); });
930
+ $('#btn_sel_dialog_cancel').on('click', function() { Popover.detach(); });
931
+ $('#btn_sel_dialog_add').on('click', function() { doAdd(); });
840
932
 
841
933
  var $input = $('#fe_sel_dialog_text').focus().on('keydown', function(event) {
842
934
  // capture enter key
@@ -848,7 +940,9 @@ var TextSelect = {
848
940
  });
849
941
 
850
942
  $elem.addClass('popped');
851
- Popover.onDetach = function() { $elem.removeClass('popped'); };
943
+ Popover.onDetach = function() {
944
+ $elem.removeClass('popped').focus();
945
+ };
852
946
  }
853
947
 
854
948
  }; // TextSelect
package/js/tools.js CHANGED
@@ -1,7 +1,7 @@
1
1
  ////
2
2
  // Joe's Misc JavaScript Tools
3
- // Copyright (c) 2004 - 2025 Joseph Huckaby
4
- // Released under the MIT License
3
+ // Copyright (c) 2004 - 2026 Joseph Huckaby
4
+ // Released under the BSD 3-Clause License
5
5
  ////
6
6
 
7
7
  var months = [
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pixl-xyapp",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "A theme for xyOps.",
5
5
  "author": "Joseph Huckaby <jhuckaby@pixlcore.com>",
6
6
  "homepage": "https://github.com/pixlcore/pixl-xyapp",
7
- "license": "MIT",
7
+ "license": "BSD-3-Clause",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/pixlcore/pixl-xyapp.git"