|
39 | 39 | var that = this,
|
40 | 40 | id = this.element.attr('id');
|
41 | 41 | this.identifiers = ['ui-' + id, 'ui-' + id];
|
| 42 | + // Append one selectgroup.group for the widget as outlined in the flyweight pattern. |
42 | 43 | if ($.ui.selectgroup.group.initialised === false) {
|
43 | 44 | $('body').append($.ui.selectgroup.group);
|
44 | 45 | $.ui.selectgroup.group.hide();
|
45 | 46 | }
|
46 | 47 | $.ui.selectgroup.group.initialised = true;
|
| 48 | + // Obtain the placeholder text either from the selected option or the first option. |
47 | 49 | if ($(this.element).find('option:selected').length) {
|
48 | 50 | this.copy = this.element.find('option:selected').text();
|
49 | 51 | }
|
50 | 52 | else {
|
51 | 53 | this.copy = this.element.find('option').first().text();
|
52 | 54 | }
|
| 55 | + // Create an element to replace the select element interface. |
53 | 56 | this.placeholder = $('<a href="#" id="' + this.identifiers[1] + '" class="' + this.widgetBaseClass + ' ui-widget ui-state-default ui-corner-all"'
|
54 | 57 | + 'role="button" aria-haspopup="true" aria-owns="' + this.widgetBaseClass + '-group">'
|
55 | 58 | + '<span class="' + this.widgetBaseClass + '-copy">'+ this.copy +'</span>'
|
56 | 59 | + '<span class="' + this.widgetBaseClass + '-icon ui-icon ui-icon-triangle-1-s"></span></a>');
|
| 60 | + // If the option to inherit classes is true then get the classnames from the select element and apply to the placeholder element. |
57 | 61 | if (this.options.classInherit.select) {
|
58 | 62 | this.placeholder.addClass(this.element.attr('class'));
|
59 | 63 | }
|
| 64 | + // If the option to use the popup style is true then we need to add the popup classes. |
60 | 65 | if (this.options.style === 'popup') {
|
61 | 66 | this.placeholder.addClass(this.widgetBaseClass + '-popup');
|
62 | 67 | this.placeholder.find('.' + this.widgetBaseClass + '-icon').removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-2-n-s');
|
63 | 68 | }
|
| 69 | + // Place the placeholder element after the select element and hide the select element. |
64 | 70 | this.element.after(this.placeholder).hide();
|
| 71 | + // Binds events to the placeholder element to match the native select element events. |
65 | 72 | this._placeholderEvents(true);
|
| 73 | + // Bind the focus event on the associated label to the placeholder element. |
66 | 74 | $('label[for="' + id + '"]').attr( 'for', this.identifiers[0] )
|
67 | 75 | .bind('click.selectgroup', function(event) {
|
68 | 76 | event.preventDefault();
|
69 | 77 | that.placeholder.focus();
|
70 | 78 | });
|
| 79 | + // Bind click event to the document that will close the selectgroup.group if it is open. |
71 | 80 | $(document).bind('click.selectmenu', function(event) {
|
72 | 81 | if (that.isOpen && !($(event.target).closest('.ui-selectgroup').length || $(event.target).closest('.ui-selectgroup-group').length)) {
|
73 | 82 | window.setTimeout( function() {
|
|
80 | 89 | },
|
81 | 90 | _placeholderEvents: function(value) {
|
82 | 91 | var that = this;
|
| 92 | + // If true then we want to bind events to the placeholder. |
83 | 93 | if (value === true) {
|
84 | 94 | this.placeholder.removeClass('ui-state-disabled')
|
85 | 95 | .bind('click.selectgroup', function(event) {
|
|
141 | 151 | $(this).removeClass('ui-state-hover');
|
142 | 152 | });
|
143 | 153 | }
|
| 154 | + // Else binding events is false and we want to repath them to do nothing. |
144 | 155 | else {
|
145 | 156 | this.placeholder.addClass('ui-state-disabled').unbind('.selectgroup');
|
146 | 157 | this.placeholder.bind('click.selectgroup', function(event) {
|
|
152 | 163 | }
|
153 | 164 | },
|
154 | 165 | _index: function() {
|
| 166 | + // Get all the options within the select element. Map their properties to an array for quick reference. |
155 | 167 | this.selectors = $.map($('option', this.element), function(value) {
|
156 | 168 | return {
|
157 | 169 | element: $(value),
|
|
169 | 181 | _renderGroup: function() {
|
170 | 182 | var that = this,
|
171 | 183 | hidden = false;
|
| 184 | + // Create the selectgroup list container. |
172 | 185 | this.group = $('<ul class="' + this.widgetBaseClass + '-list" role="listbox" aria-hidden="true"></ul>');
|
173 | 186 | if (this.options.autoWidth) {
|
174 | 187 | if (this.options.style === 'dropdown') {
|
|
181 | 194 | if (this.options.style === 'popup') {
|
182 | 195 | this.group.addClass(this.widgetBaseClass + '-popup');
|
183 | 196 | }
|
| 197 | + // Render the options available within the select. |
184 | 198 | this._renderOption();
|
185 | 199 | this.group.attr('aria-labelledby', this.identifiers[0]);
|
186 | 200 | $($.ui.selectgroup.group).html(this.group);
|
187 | 201 | },
|
188 | 202 | _renderOption: function() {
|
189 | 203 | var that = this;
|
| 204 | + // Iterate through the selectors array which is a map of the options within the select element. |
190 | 205 | $.each(this.selectors, function(index) {
|
191 | 206 | var self = this;
|
| 207 | + // Each option will be represented as a list item. Bind properties and events to each list item. |
192 | 208 | var list = $('<li role="presentation"><a role="option" href="#">'+ this.text +'</a></li>')
|
193 | 209 | .bind('click.selectgroup', function(event) {
|
194 | 210 | event.preventDefault();
|
|
219 | 235 | if (typeof this.disabled !== "undefined" && this.disabled === 'disabled') {
|
220 | 236 | list.addClass('ui-state-disabled');
|
221 | 237 | }
|
| 238 | + // The option may be part of an option group. |
222 | 239 | if (this.optgroup.length) {
|
223 | 240 | var name = that.widgetBaseClass + '-optgroup-' + that.element.find('optgroup').index(this.optgroup);
|
| 241 | + // If the option group has already been rendered append the option list item to the option group list. |
224 | 242 | if (that.group.find('li.' + name).length ) {
|
225 | 243 | that.group.find('li.' + name + ' ul').append(list);
|
226 | 244 | }
|
| 245 | + // Else we need to create a representation of an option group and then append the option list item. |
227 | 246 | else {
|
228 | 247 | var opt = $('<li class="' + name + ' ' + that.widgetBaseClass + '-optgroup"><span>'+ this.optgroup.attr('label') +'</span><ul></ul></li>');
|
229 | 248 | if (that.options.classInherit.optionGroup) {
|
|
237 | 256 | }
|
238 | 257 | }
|
239 | 258 | }
|
| 259 | + // Else it is not part of an option group and we can append directly to the selectgroup list container. |
240 | 260 | else {
|
241 | 261 | list.appendTo(that.group);
|
242 | 262 | }
|
|
246 | 266 | var options = this.options,
|
247 | 267 | local = this.group.find('li').not('.ui-selectgroup-optgroup'),
|
248 | 268 | instance = local.get(this.position);
|
| 269 | + // Reset the selectgroup.group position. |
249 | 270 | $($.ui.selectgroup.group).css({'top': 0, 'left': 0});
|
250 |
| - $($.ui.selectgroup.group).show() |
| 271 | + $($.ui.selectgroup.group).show(); |
| 272 | + // Adjust position if it is a popup. |
251 | 273 | if (this.options.style === 'popup' && !this.options.positioning.offset) {
|
252 | 274 | var adjust = '0 -' + ($(instance).outerHeight() + $(instance).offset().top);
|
253 | 275 | }
|
| 276 | + // Position the selectgroup.group using the jQuery UI position widget and any applied options. |
254 | 277 | $($.ui.selectgroup.group).position({
|
255 | 278 | of: options.positioning.of || this.placeholder,
|
256 | 279 | my: options.positioning.my,
|
|
260 | 283 | });
|
261 | 284 | },
|
262 | 285 | _toggle: function() {
|
| 286 | + // Toggle the selectgroup to open or close. |
263 | 287 | if ($.ui.selectgroup.group.past !== null) {
|
264 | 288 | if ($.ui.selectgroup.group.past.element !== this.element) {
|
265 | 289 | this.focus();
|
|
296 | 320 | position = this.position,
|
297 | 321 | instance = null;
|
298 | 322 | position = this.position + value;
|
299 |
| - if (position < 0) { |
300 |
| - position = 0; |
301 |
| - } |
302 |
| - if (position > maximum) { |
303 |
| - position = maximum; |
304 |
| - } |
305 |
| - if (position === record) { |
306 |
| - return; |
307 |
| - } |
308 |
| - if (this.selectors[position].disabled === 'disabled' || this.selectors[position].optDisabled === 'disabled') { |
309 |
| - if (value > 0) { |
310 |
| - ++value; |
311 |
| - } |
312 |
| - else { |
313 |
| - --value; |
314 |
| - } |
315 |
| - this._traverse(value, position); |
| 323 | + // Traverse the selectgroup based on a numerical iteration. |
| 324 | + if (position < 0) { |
| 325 | + position = 0; |
| 326 | + } |
| 327 | + if (position > maximum) { |
| 328 | + position = maximum; |
| 329 | + } |
| 330 | + // If we have traversed through the positions and not found a new one due to a disabled state we can return to the last available position. |
| 331 | + if (position === record) { |
| 332 | + return; |
| 333 | + } |
| 334 | + if (this.selectors[position].disabled === 'disabled' || this.selectors[position].optDisabled === 'disabled') { |
| 335 | + if (value > 0) { |
| 336 | + ++value; |
316 | 337 | }
|
317 | 338 | else {
|
318 |
| - this.position = position; |
319 |
| - instance = local.get(this.position); |
320 |
| - this.copy = $(instance).find('a').text(); |
321 |
| - local.removeClass('ui-state-hover'); |
322 |
| - $(instance).addClass('ui-state-hover'); |
323 |
| - this.placeholder.find('.ui-selectgroup-copy').text(this.copy); |
324 |
| - this.element.find('option:selected').removeAttr('selected'); |
325 |
| - $(this.selectors[this.position].element).attr('selected', 'selected'); |
| 339 | + --value; |
326 | 340 | }
|
| 341 | + // The next position may be disabled. Iterate through until a position is found, zero or maxmimum position reached. |
| 342 | + this._traverse(value, position); |
| 343 | + } |
| 344 | + else { |
| 345 | + this.position = position; |
| 346 | + instance = local.get(this.position); |
| 347 | + this.copy = $(instance).find('a').text(); |
| 348 | + local.removeClass('ui-state-hover'); |
| 349 | + $(instance).addClass('ui-state-hover'); |
| 350 | + this.placeholder.find('.ui-selectgroup-copy').text(this.copy); |
| 351 | + this.element.find('option:selected').removeAttr('selected'); |
| 352 | + $(this.selectors[this.position].element).attr('selected', 'selected'); |
| 353 | + } |
327 | 354 | $.ui.selectgroup.group.position = value;
|
328 | 355 | },
|
329 | 356 | _typeahead: function(character) {
|
|
332 | 359 | local = this.group.find('li').not('.ui-selectgroup-optgroup'),
|
333 | 360 | instance = null,
|
334 | 361 | found = false;
|
| 362 | + // Type ahead based on an alpha numeric charater input. |
335 | 363 | character = character.toLowerCase();
|
336 | 364 | this.search[1] += character;
|
337 | 365 | window.clearTimeout(this.timer);
|
|
346 | 374 | found = true;
|
347 | 375 | that.search[3] = index;
|
348 | 376 | };
|
| 377 | + // Typing ahead we need to remember an array of possible word states based on time and character position. |
| 378 | + // Is the new character a repetition of the first character. |
349 | 379 | if (this.search[0] === this.search[1][0]) {
|
| 380 | + // Are we attempting to iterate through the selectors using the same character? |
350 | 381 | if (this.search[1].length < 2) {
|
351 | 382 | $.each(this.selectors, function(index) {
|
352 | 383 | if (!found) {
|
|
361 | 392 | });
|
362 | 393 | this.search[0] = this.search[1][0];
|
363 | 394 | }
|
| 395 | + // Or are we navigating to a character combination based on the first character? |
364 | 396 | else {
|
365 | 397 | $.each(this.selectors, function(index) {
|
366 | 398 | if (!found) {
|
|
377 | 409 | }
|
378 | 410 | }
|
379 | 411 | else {
|
| 412 | + // Iterate through the selectors with the new unrepeated character. |
380 | 413 | $.each(this.selectors, function(index) {
|
381 | 414 | if (!found) {
|
382 | 415 | if (that.search[1] === that.selectors[index].text.substring(0, that.search[1].length).toLowerCase()) {
|
|
387 | 420 | });
|
388 | 421 | this.search[0] = this.search[1][0];
|
389 | 422 | }
|
| 423 | + // If the end of available matches is meet we can reposition to the start of available matches. |
390 | 424 | if (that.search[4] === that.search[3]) {
|
391 | 425 | that.search[3] = that.search[2];
|
392 | 426 | focusOption(that.search[3]);
|
393 | 427 | }
|
394 | 428 | this.search[4] = this.search[3];
|
| 429 | + // Clear the initial search after 1 second. |
395 | 430 | this.timer = window.setTimeout(function() {that.search[1] = '';}, (1000));
|
396 | 431 | },
|
397 | 432 | destroy: function() {
|
| 433 | + // Remove the selectgroup completely. |
| 434 | + // Has known issues. |
398 | 435 | var id = this.identifiers[0].split('ui-')
|
399 | 436 | if (this.isOpen) {
|
400 | 437 | this.close();
|
|
407 | 444 | this.element.show();
|
408 | 445 | },
|
409 | 446 | enable: function(index, type) {
|
| 447 | + // Enable the placeholder, an option group or option in the original select. |
| 448 | + // Trigger a 'change' event to redraw the selectgroup. |
410 | 449 | if (this.isOpen) {
|
411 | 450 | this.close();
|
412 | 451 | }
|
|
423 | 462 | }
|
424 | 463 | },
|
425 | 464 | disable: function(index, type) {
|
| 465 | + // Disable the placeholder, an option group or option in the original select. |
| 466 | + // Trigger a 'change' event to redraw the selectgroup. |
426 | 467 | if (this.isOpen) {
|
427 | 468 | this.close();
|
428 | 469 | }
|
|
439 | 480 | }
|
440 | 481 | },
|
441 | 482 | focus: function() {
|
| 483 | + // Focus on the placeholder to activate without opening the selectgroup.group. |
442 | 484 | this._index();
|
443 | 485 | this._renderGroup();
|
444 | 486 | this.isActive = true;
|
445 | 487 | },
|
446 | 488 | blur: function() {
|
| 489 | + // After losing focus on the placeholder. |
447 | 490 | this.isActive = false;
|
448 | 491 | },
|
449 | 492 | change: function() {
|
| 493 | + // Redraw the selectgroup.group. |
450 | 494 | this._index();
|
451 | 495 | this._renderGroup();
|
452 | 496 | },
|
453 | 497 | refresh: function() {
|
| 498 | + // Refresh the placeholder. |
454 | 499 | if ($(this.element).find('option:selected').length) {
|
455 | 500 | this.copy = this.element.find('option:selected').text();
|
456 | 501 | }
|
|
460 | 505 | this.placeholder.find('.ui-selectgroup-copy').text(this.copy);
|
461 | 506 | },
|
462 | 507 | open: function() {
|
| 508 | + // Open the selectgroup.group. |
463 | 509 | if (this.options.style === 'dropdown') {
|
464 | 510 | $.ui.selectgroup.group.removeClass('ui-corner-all').addClass('ui-corner-bottom');
|
465 | 511 | this.placeholder.removeClass('ui-corner-all').addClass('ui-state-active ui-corner-top');
|
|
473 | 519 | this.isOpen = true;
|
474 | 520 | },
|
475 | 521 | close: function() {
|
| 522 | + // Close the selectgroup.group. |
476 | 523 | if ($.ui.selectgroup.group.past !== null) {
|
477 | 524 | $.ui.selectgroup.group.past.placeholder.removeClass('ui-state-active');
|
478 | 525 | }
|
|
486 | 533 | this.group.attr('aria-hidden', 'true');
|
487 | 534 | this.isOpen = false;
|
488 | 535 | }
|
489 |
| - }) |
| 536 | + }); |
| 537 | + // Publically available objects used once by the selectgroup to enable the flyweight pattern. |
490 | 538 | $.ui.selectgroup.group = $('<div class="ui-selectgroup-group ui-widget ui-widget-content ui-corner-all"></div>');
|
491 | 539 | $.ui.selectgroup.group.initialised = false;
|
492 | 540 | $.ui.selectgroup.group.past = null;
|
|
0 commit comments