pixl-xyapp 2.1.6 → 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/README.md CHANGED
@@ -355,7 +355,7 @@ The pagination links work by constructing self-referencing URL to the current pa
355
355
 
356
356
  Since the `limit` is set to 5 items per page, and `offset` starts at `0`, then the next page (page 2) will be at offset `5`. This link is simply a hashtag anchor tag, which doesn't reload the browser page, but will instead be caught by the navigation system, and call your page's `onDeactivate()` then its `onActivate()` with the new values. It is up to your page code to redraw the table with the new data chunk and new `offset` value.
357
357
 
358
- Instead of generating hashtag anchor links, you can optionally provide a custom JavaScript function in a `pagination_link` property, which will be written into the HTML as an `onMouseUp` handler on each link, and called instead of a standard link. Note that it must be a string and globally accessible, so remember the `$P()` shortcut to get access to the current page. Example:
358
+ Instead of generating hashtag anchor links, you can optionally provide a custom JavaScript function in a `pagination_link` property, which will be written into the HTML as an `onClick` handler on each link, and called instead of a standard link. Note that it must be a string and globally accessible, so remember the `$P()` shortcut to get access to the current page. Example:
359
359
 
360
360
  ```javascript
361
361
  pagination_link: '$P().tableNavClick'
@@ -370,7 +370,7 @@ Notification messages are shown in a fixed bar at the top of the screen, regardl
370
370
  To use the notification system in your app, make sure this markup is in your main HTML page:
371
371
 
372
372
  ```html
373
- <div id="d_message" class="message" style="display:none" onMouseUp="app.hideMessage(250)">
373
+ <div id="d_message" class="message" style="display:none" onClick="app.hideMessage(250)">
374
374
  <div id="d_message_inner" class="message_inner"></div>
375
375
  </div>
376
376
  ```
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;
@@ -112,22 +112,22 @@ body.dark {
112
112
  --gray: rgb(98, 107, 115);
113
113
  --gray-highlight: color-mix(in srgb, rgb(98, 107, 115) 80%, white);
114
114
 
115
- --border-color: rgb(50, 54, 58);
115
+ --border-color: rgb(50, 50, 58);
116
116
  --shadow-color: rgba(0, 0, 0, 0.0);
117
- --background-color: rgb(24, 28, 32);
118
- --header-background-color: rgb(32, 34, 38);
119
- --header-text-color: rgb(125, 135, 150);
120
- --box-background-color: rgb(32, 34, 38);
121
- --form-background-color: rgb(40, 44, 48);
117
+ --background-color: rgb(24, 24, 32);
118
+ --header-background-color: rgb(32, 32, 38);
119
+ --header-text-color: rgb(125, 125, 150);
120
+ --box-background-color: rgb(32, 32, 38);
121
+ --form-background-color: rgb(40, 40, 48);
122
122
  --table-header-background-color: rgba(200, 225, 255, 0.03);
123
123
 
124
- --sidebar-background-color: rgb(32, 34, 38);
125
- --sidebar-text-color: rgb(125, 135, 150);
124
+ --sidebar-background-color: rgb(32, 32, 38);
125
+ --sidebar-text-color: rgb(125, 125, 150);
126
126
 
127
- --body-text-color: rgb(170, 180, 200);
128
- --link-color: rgb(145, 155, 170);
129
- --label-color: rgb(145, 155, 170);
130
- --alt-label-color: rgb(120, 125, 130);
127
+ --body-text-color: rgb(170, 170, 200);
128
+ --link-color: rgb(145, 145, 170);
129
+ --label-color: rgb(145, 145, 170);
130
+ --alt-label-color: rgb(120, 120, 130);
131
131
  --icon-color: rgb(115, 115, 130);
132
132
  --icon-color-half: rgba(115, 115, 130, 0.5);
133
133
  }
@@ -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
  }
@@ -695,17 +709,12 @@ div.toast > span {
695
709
  word-break: break-word;
696
710
  }
697
711
 
698
- div.toast.success { background: #44bb44; }
699
- div.toast.warning { background: #bbbb44; }
712
+ div.toast.success { background: var(--green); }
713
+ div.toast.warning { background: var(--yellow); }
700
714
  div.toast.error { background: var(--red); }
701
- div.toast.critical { background: #bb44bb; }
702
- div.toast.info { background: #5890db; }
703
-
704
- body.dark div.toast.success { background: rgb(20, 128, 20); }
705
- body.dark div.toast.warning { background: rgb(128, 128, 20); }
706
- body.dark div.toast.error { background-color:var(--red); }
707
- body.dark div.toast.critical { background: rgb(128, 20, 128); }
708
- body.dark div.toast.info { background: rgb(28, 84, 160); }
715
+ div.toast.critical { background: var(--purple); }
716
+ div.toast.info { background: var(--theme-color); }
717
+ div.toast.suspended { background: var(--orange); }
709
718
 
710
719
  div.toast.channel {
711
720
  background: var(--box-background-color);
@@ -787,6 +796,7 @@ div.message.error { background: var(--red); }
787
796
  div.message.critical { background: var(--purple); }
788
797
  div.message.abort { background: var(--red); }
789
798
  div.message.info { background: var(--blue); }
799
+ div.message.suspended { background: var(--orange); }
790
800
 
791
801
  /* body.dark div.message.success { background: rgb(20, 128, 20); }
792
802
  body.dark div.message.warning { background: rgb(128, 128, 20); }
@@ -1494,9 +1504,6 @@ body.dark fieldset .checkbox_container > label:before {
1494
1504
  transform: scale(.2);
1495
1505
  }
1496
1506
 
1497
- /* .checkbox_container > input:hover ~ label:before {
1498
- border-color: var(--theme-color);
1499
- } */
1500
1507
  .checkbox_container > input:hover ~ label {
1501
1508
  color: var(--theme-color);
1502
1509
  }
@@ -1521,13 +1528,11 @@ body.dark fieldset .checkbox_container > label:before {
1521
1528
  .checkbox_container:hover {
1522
1529
  color: var(--body-text-color);
1523
1530
  }
1524
- /* .checkbox_container > input:hover ~ label:before {
1525
- background-color: var(--theme-color-half) !important;
1526
- border-color: var(--theme-color);
1527
- } */
1528
- /* .checkbox_container > input[checked]:hover ~ label:after {
1529
- background-color: var(--theme-color-half) !important;
1530
- } */
1531
+
1532
+ .checkbox_container > input:focus-visible ~ label::before {
1533
+ outline: 2px solid var(--theme-color);
1534
+ outline-offset: 2px;
1535
+ }
1531
1536
 
1532
1537
  div.info_label {
1533
1538
  font-size: 11px;
@@ -1911,6 +1916,10 @@ div.progress_bar_label.second_half {
1911
1916
  text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4);
1912
1917
  }
1913
1918
 
1919
+ .progress_bar_label i.mdi:before {
1920
+ transform: scale(1.5);
1921
+ }
1922
+
1914
1923
  div.progress_bar_container.indeterminate div.progress_bar_label {
1915
1924
  display: none;
1916
1925
  }
@@ -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
 
@@ -2587,6 +2597,10 @@ body.dark .data_grid > ul.grid_row_header > div {
2587
2597
  padding-right: 1px;
2588
2598
  }
2589
2599
 
2600
+ .data_grid > ul > div .progress_bar_container {
2601
+ margin: 1px 0 2px 0; /* Nudge height from 18px to 21px to match std grid units */
2602
+ }
2603
+
2590
2604
  .data_grid ul.disabled div {
2591
2605
  opacity: 0.6;
2592
2606
  }
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;
@@ -225,9 +225,11 @@ var app = {
225
225
  var icon = '';
226
226
  switch (type) {
227
227
  case 'success': icon = 'check-circle'; break;
228
- case 'warning': icon = 'alert-circle'; break;
228
+ case 'warning': icon = 'alert-rhombus'; break;
229
229
  case 'error': icon = 'alert-decagram'; break;
230
230
  case 'info': icon = 'information-outline'; break;
231
+ case 'critical': icon = 'fire-alert'; break;
232
+ case 'suspended': icon = 'motion-pause-outline'; break;
231
233
 
232
234
  default:
233
235
  if (type.match(/^(\w+)\/(.+)$/)) { type = RegExp.$1; icon = RegExp.$2; }
@@ -245,8 +247,11 @@ var app = {
245
247
  // show toast notification given raw html
246
248
  var { type, icon, msg, lifetime, loc } = args;
247
249
 
250
+ var role = 'alert';
251
+ if ((type == 'success') || (type == 'info')) role = 'status';
252
+
248
253
  var html = '';
249
- html += '<div class="toast ' + type + '" style="display:none">';
254
+ html += '<div class="toast ' + type + '" style="display:none" role="' + role + '" aria-atomic="true">';
250
255
  html += '<i class="mdi mdi-' + icon + '"></i>';
251
256
  html += '<span>' + msg + '</span>';
252
257
  html += '</div>';
@@ -259,7 +264,11 @@ var app = {
259
264
  $toast.on('click', function() {
260
265
  if (timer) clearTimeout(timer);
261
266
  $toast.fadeOut( 250, function() { $(this).remove(); } );
262
- if (loc) Nav.go(loc);
267
+ if (loc) {
268
+ $toast.addClass('clicky');
269
+ if (typeof(loc) == 'function') loc();
270
+ else if (typeof(loc) == 'string') Nav.go(loc);
271
+ }
263
272
  } );
264
273
 
265
274
  if ((type == 'success') || (type == 'info') || lifetime) {
@@ -271,9 +280,7 @@ var app = {
271
280
  },
272
281
 
273
282
  hideMessage: function(animate) {
274
- // if (animate) $('#d_message').hide(animate);
275
- // else $('#d_message').hide();
276
-
283
+ // hide all toasts
277
284
  if (animate) $('div.toast').fadeOut( animate, function() { $(this).remove(); } );
278
285
  else $('div.toast').remove();
279
286
  },
@@ -532,6 +539,47 @@ var app = {
532
539
  notifyUserNav: function(loc) {
533
540
  // override in app
534
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
+ }
535
583
  }
536
584
 
537
585
  }; // app object
package/js/calendar.js CHANGED
@@ -46,9 +46,9 @@ var Calendar = {
46
46
 
47
47
  html += '<div class="calendar">';
48
48
  html += '<div class="cal_header">';
49
- html += '<div class="ch_nav ch_prev" onMouseUp="Calendar.prevMonth()" title="Previous Month"><i class="mdi mdi-chevron-double-left"></i></div>';
49
+ html += '<div class="ch_nav ch_prev" onClick="Calendar.prevMonth()" title="Previous Month"><i class="mdi mdi-chevron-double-left"></i></div>';
50
50
  html += '<div class="ch_title"></div>';
51
- html += '<div class="ch_nav ch_next" onMouseUp="Calendar.nextMonth()" title="Next Month"><i class="mdi mdi-chevron-double-right"></i></div>';
51
+ html += '<div class="ch_nav ch_next" onClick="Calendar.nextMonth()" title="Next Month"><i class="mdi mdi-chevron-double-right"></i></div>';
52
52
  html += '</div>';
53
53
  html += '<div class="cal_days">';
54
54
  _short_day_names.forEach( function(ddd) {
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;
@@ -557,7 +557,7 @@ window.Page = class Page {
557
557
  // html += 'Page: ';
558
558
  if (current_page > 1) {
559
559
  if (cpl) {
560
- html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
560
+ html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
561
561
  }
562
562
  else {
563
563
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -587,7 +587,7 @@ window.Page = class Page {
587
587
  }
588
588
  else {
589
589
  if (cpl) {
590
- html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
590
+ html += '<span class="link" onClick="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
591
591
  }
592
592
  else {
593
593
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -601,7 +601,7 @@ window.Page = class Page {
601
601
  html += '&nbsp;&nbsp;';
602
602
  if (current_page < num_pages) {
603
603
  if (cpl) {
604
- html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
604
+ html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
605
605
  }
606
606
  else {
607
607
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -717,7 +717,7 @@ window.Page = class Page {
717
717
  // html += 'Page: ';
718
718
  if (current_page > 1) {
719
719
  if (cpl) {
720
- html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
720
+ html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
721
721
  }
722
722
  else {
723
723
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -748,7 +748,7 @@ window.Page = class Page {
748
748
  }
749
749
  else {
750
750
  if (cpl) {
751
- html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
751
+ html += '<span class="link" onClick="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
752
752
  }
753
753
  else {
754
754
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -763,7 +763,7 @@ window.Page = class Page {
763
763
 
764
764
  if (current_page < num_pages) {
765
765
  if (cpl) {
766
- html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
766
+ html += '<span class="link" onClick="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
767
767
  }
768
768
  else {
769
769
  html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
@@ -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 = [
@@ -560,7 +560,7 @@ function expando_text(text, max, link) {
560
560
  var after = text.substring(max);
561
561
 
562
562
  return before +
563
- '<span>... <a href="javascript:void(0)" onMouseUp="$(this).parent().hide().next().show()">'+link+'</a></span>' +
563
+ '<span>... <a href="javascript:void(0)" onClick="$(this).parent().hide().next().show()">'+link+'</a></span>' +
564
564
  '<span style="display:none">' + after + '</span>';
565
565
  };
566
566
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "pixl-xyapp",
3
- "version": "2.1.6",
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
- "url": "https://github.com/pixlcore/pixl-xyapp"
10
+ "url": "git+https://github.com/pixlcore/pixl-xyapp.git"
11
11
  },
12
12
  "bugs": {
13
13
  "url": "https://github.com/pixlcore/pixl-xyapp/issues"