| [ Index ] |
PHP Cross Reference of Drupal 6 (gatewave) |
[Summary view] [Print] [Text view]
1 // $Id: options_element.js,v 1.8 2009/01/13 03:39:56 quicksketch Exp $ 2 3 /** 4 * @file 5 * Add JavaScript behaviors for the "options" form element type. 6 */ 7 Drupal.behaviors.optionsElement = function(context) { 8 $('div.form-options:not(.options-element-processed)', context).each(function() { 9 $(this).addClass('options-element-processed'); 10 new Drupal.optionsElement(this); 11 }); 12 }; 13 14 /** 15 * Constructor for an options element. 16 */ 17 Drupal.optionsElement = function(element) { 18 var self = this; 19 20 // Find the original "manual" fields. 21 this.element = element; 22 this.manualElement = $(element).find('fieldset.options').get(0); 23 this.manualOptionsElement = $(element).find('textarea').get(0); 24 this.manualDefaultValueElement = $(element).find('input.form-text').get(0); 25 this.keyTypeToggle = $(element).find('input.key-type-toggle').get(0); 26 this.multipleToggle = $(element).find('input.multiple-toggle').get(0); 27 28 // Setup variables containing the current status of the widget. 29 this.optgroups = $(element).is('.options-optgroups'); 30 this.multiple = $(element).is('.options-multiple'); 31 this.keyType = element.className.replace(/^.*?options-key-type-([a-z]+).*?$/, '$1'); 32 this.identifier = this.manualOptionsElement.id + '-widget'; 33 34 // Warning messages. 35 this.keyChangeWarning = Drupal.t('Custom keys have been specified in this list. Removing these custom keys may change way data is stored. Are you sure you wish to remove these custom keys?'); 36 37 // Setup new DOM elements containing the actual options widget. 38 this.optionsElement = $('<div></div>').get(0); // Temporary DOM object. 39 this.optionsToggleElement = $(Drupal.theme('optionsElementToggle')).get(0); 40 41 // Add the options widget and toggle elements to the page. 42 $(this.manualElement).hide().before(this.optionsElement).after(this.optionsToggleElement); 43 44 // Add a toggle action for manual entry of options. 45 $(this.optionsToggleElement).find('a').click(function() { 46 self.toggleMode(); 47 return false; 48 }); 49 50 // Add a handler for key type changes. 51 if (this.keyTypeToggle) { 52 $(this.keyTypeToggle).change(function(){ 53 var checked = $(this).attr('checked'); 54 // Before switching to associative keys, ensure we're not destorying 55 // any custom specified keys. 56 if (!checked) { 57 var options = self.optionsFromText(); 58 var confirm = false; 59 for (var n in options) { 60 if (options[n].key != options[n].value) { 61 confirm = true; 62 break; 63 } 64 } 65 if (confirm) { 66 if (window.confirm(self.keyChangeWarning)) { 67 self.setKeyType('associative'); 68 } 69 } 70 else { 71 self.setKeyType('associative'); 72 } 73 } 74 else { 75 self.setKeyType('custom'); 76 } 77 }); 78 } 79 80 // Add a handler for multiple value changes. 81 if (this.multipleToggle) { 82 $(this.multipleToggle).change(function(){ 83 self.setMultiple($(this).attr('checked')); 84 }); 85 } 86 87 // Update the options widget with the current state of the textarea. 88 this.updateWidgetElements(); 89 } 90 91 /** 92 * Update the widget element based on the current values of the manual elements. 93 */ 94 Drupal.optionsElement.prototype.updateWidgetElements = function () { 95 var self = this; 96 97 // Create a new options element and replace the existing one. 98 var newElement = $(Drupal.theme('optionsElement', this)).get(0); 99 if ($(this.optionsElement).is(':not(:visible)')) { 100 $(newElement).css('display', 'none'); 101 } 102 $(this.optionsElement).replaceWith(newElement); 103 this.optionsElement = newElement; 104 105 // Manually setup table drag for the created table. 106 Drupal.settings.tableDrag = Drupal.settings.tableDrag || {}; 107 Drupal.settings.tableDrag[this.identifier] = { 108 'option-depth': { 109 0: { 110 action: 'depth', 111 hidden: false, 112 limit: 0, 113 relationship: 'self', 114 source: 'option-depth', 115 target: 'option-depth', 116 } 117 } 118 }; 119 120 // Allow indentation of elements if optgroups are supported. 121 if (this.optgroups) { 122 Drupal.settings.tableDrag[this.identifier]['option-parent'] = { 123 0: { 124 action: 'match', 125 hidden: false, 126 limit: 1, 127 relationship: 'parent', 128 source: 'option-key', 129 target: 'option-parent', 130 } 131 }; 132 } 133 134 // Enable button for adding options. 135 $('a.add', this.optionElement).click(function() { 136 self.addOption($(this).parents('tr:first').get(0)); 137 return false; 138 }); 139 140 // Enable button for removing options. 141 $('a.remove', this.optionElement).click(function() { 142 self.removeOption($(this).parents('tr:first').get(0)); 143 return false; 144 }); 145 146 // Add the same update action to all textfields and radios. 147 $('input', this.optionsElement).change(function() { 148 self.updateOptionElements(); 149 self.updateManualElements(); 150 }); 151 152 // Add a delayed update to textfields. 153 $('input.option-value', this.optionsElement).keyup(function(e) { 154 self.pendingUpdate(e); 155 }); 156 157 // Attach behaviors as normal to the new widget. 158 Drupal.attachBehaviors(this.optionsElement); 159 160 // Add an onDrop action to the table drag instance. 161 Drupal.tableDrag[this.identifier].onDrop = function() { 162 // Update the checkbox/radio buttons for selecting default values. 163 if (self.optgroups) { 164 self.updateOptionElements(); 165 } 166 // Update the options within the hidden text area. 167 self.updateManualElements(); 168 }; 169 170 // Add an onIndent action to the table drag row instances. 171 Drupal.tableDrag[this.identifier].row.prototype.onIndent = function() { 172 if (this.indents) { 173 $(this.element).addClass('indented'); 174 } 175 else { 176 $(this.element).removeClass('indented'); 177 } 178 }; 179 180 // Set the tab indexes. 181 this.updateOptionElements(); 182 } 183 184 /** 185 * Update the original form elment based on the current state of the widget. 186 */ 187 Drupal.optionsElement.prototype.updateManualElements = function() { 188 var options = {}; 189 190 // Build a list of current options. 191 var previousOption = false; 192 $(this.optionsElement).find('input.option-value').each(function() { 193 var $row = $(this).is('tr') ? $(this) : $(this).parents('tr:first'); 194 var depth = $row.find('input.option-depth').val(); 195 if (depth == 1 && previousOption) { 196 if (typeof(options[previousOption]) != 'object') { 197 options[previousOption] = {}; 198 } 199 options[previousOption][this.value] = this.value; 200 } 201 else { 202 options[this.value] = this.value; 203 previousOption = this.value; 204 } 205 }); 206 this.options = options; 207 208 // Update the default value. 209 var defaultValue = this.multiple ? [] : ''; 210 var multiple = this.multiple; 211 $(this.optionsElement).find('input.option-default').each(function() { 212 if (this.checked) { 213 if (multiple) { 214 defaultValue.push(this.value); 215 } 216 else { 217 defaultValue = this.value; 218 } 219 } 220 }); 221 this.defaultValue = defaultValue; 222 223 // Update with the new text and trigger the change action on the field. 224 this.optionsToText(); 225 $(this.manualOptionsElement).change(); 226 } 227 228 /** 229 * Several maintenance routines to update all rows of the options element. 230 * 231 * - Disable options for optgroups if indented. 232 * - Disable add and delete links if indented. 233 * - Match the default value radio button value to the key of the text element. 234 * - Reset the taborder. 235 */ 236 Drupal.optionsElement.prototype.updateOptionElements = function() { 237 var self = this; 238 var previousElement = false; 239 var $rows = $(this.optionsElement).find('tr'); 240 241 $rows.each(function(index) { 242 // Update the elements key if matching the key and value. 243 if (self.keyType == 'associative') { 244 var optionValue = $(this).find('input.option-value').val(); 245 $(this).find('input.option-key').val(optionValue); 246 } 247 248 // Match the default value checkbox/radio button to the option's key. 249 var optionKey = $(this).find('input.option-key').val(); 250 $(this).find('input.option-default').val(optionKey); 251 252 // Set the tab order. 253 $(this).find('input.form-text').attr('tabindex', index + 1); 254 255 // Hide the add/remove links the row if indented. 256 var depth = $(this).find('input.option-depth').val(); 257 var parent = $(this).find('input.option-parent').val(); 258 var defaultInput = $(this).find('input.option-default').get(0); 259 260 if (depth == 1) { 261 $(previousElement).attr('disabled', true).attr('checked', false); 262 $(previousElement).parents('tr:first').find('a.add, a.remove').hide(); 263 $(defaultInput).parents('tr:first').find('a.add, a.remove').show(); 264 $(defaultInput).attr('disabled', false); 265 } 266 else { 267 $(defaultInput).attr('disabled', false); 268 $(defaultInput).parents('tr:first').find('a.add, a.remove').show(); 269 previousElement = defaultInput; 270 } 271 }); 272 273 // Do not allow the last item to be removed. 274 if ($rows.size() == 1) { 275 $rows.find('a.remove').hide(); 276 } 277 } 278 279 /** 280 * Add a new option below the current row. 281 */ 282 Drupal.optionsElement.prototype.addOption = function(currentOption) { 283 var self = this; 284 var newOption = $(currentOption).clone() 285 .find('input.option-key').val(self.keyType == 'numeric' ? self.nextNumericKey() : '').end() 286 .find('input.option-value').val('').end() 287 .find('input.option-default').attr('checked', false).end() 288 .find('a.tabledrag-handle').remove().end() 289 .removeClass('drag-previous') 290 .insertAfter(currentOption) 291 .get(0); 292 293 // Make the new option draggable. 294 Drupal.tableDrag[this.identifier].makeDraggable(newOption); 295 296 // Enable button for adding options. 297 $('a.add', newOption).click(function() { 298 self.addOption(newOption); 299 return false; 300 }); 301 302 // Enable buttons for removing options. 303 $('a.remove', newOption).click(function() { 304 self.removeOption(newOption); 305 return false; 306 }); 307 308 // Add the update action to all textfields and radios. 309 $('input', newOption).change(function() { 310 self.updateOptionElements(); 311 self.updateManualElements(); 312 }); 313 314 // Add a delayed update to textfields. 315 $('input.option-value', newOption).keyup(function(e) { 316 self.pendingUpdate(e); 317 }); 318 319 this.updateOptionElements(); 320 this.updateManualElements(); 321 } 322 323 /** 324 * Remove the current row. 325 */ 326 Drupal.optionsElement.prototype.removeOption = function(currentOption) { 327 $(currentOption).remove(); 328 329 this.updateOptionElements(); 330 this.updateManualElements(); 331 } 332 333 /** 334 * Toggle link for switching between the JavaScript and manual entry. 335 */ 336 Drupal.optionsElement.prototype.toggleMode = function() { 337 if ($(this.optionsElement).is(':visible')) { 338 var height = $(this.optionsElement).height(); 339 $(this.optionsElement).hide(); 340 $(this.manualElement).show().find('textarea').height(height); 341 $(this.optionsToggleElement).find('a').html(Drupal.t('Normal entry')); 342 } 343 else { 344 this.updateWidgetElements(); 345 $(this.optionsElement).show(); 346 $(this.manualElement).hide(); 347 $(this.optionsToggleElement).find('a').html(Drupal.t('Manual entry')); 348 } 349 } 350 351 /** 352 * Change the current key type (associative, custom, numeric, none). 353 */ 354 Drupal.optionsElement.prototype.setKeyType = function(type) { 355 $(this.element) 356 .removeClass('options-key-type-' + this.keyType) 357 .addClass('options-key-type-' + type); 358 this.keyType = type; 359 // Rebuild the options widget. 360 this.updateManualElements(); 361 this.updateWidgetElements(); 362 } 363 364 /** 365 * Set the element's #multiple property. Boolean TRUE or FALSE. 366 */ 367 Drupal.optionsElement.prototype.setMultiple = function(multiple) { 368 if (multiple) { 369 $(this.element).addClass('options-multiple'); 370 } 371 else { 372 // Unselect all default options except the first. 373 $(this.optionsElement).find('input.option-default:checked:not(:first)').attr('checked', false); 374 this.updateManualElements(); 375 $(this.element).removeClass('options-multiple'); 376 } 377 this.multiple = multiple; 378 // Rebuild the options widget. 379 this.updateWidgetElements(); 380 } 381 382 /** 383 * Update a field after a delay. 384 * 385 * Similar to immediately changing a field, this field as pending changes that 386 * will be updated after a delay. This includes textareas and textfields in 387 * which updating continuously would be a strain the server and actually slow 388 * down responsiveness. 389 */ 390 Drupal.optionsElement.prototype.pendingUpdate = function(e) { 391 var self = this; 392 393 // Only operate on "normal" keys, excluding special function keys. 394 // http://protocolsofmatrix.blogspot.com/2007/09/javascript-keycode-reference-table-for.html 395 if (!( 396 e.keyCode >= 48 && e.keyCode <= 90 || // 0-9, A-Z. 397 e.keyCode >= 93 && e.keyCode <= 111 || // Number pad. 398 e.keyCode >= 186 && e.keyCode <= 222 || // Symbols. 399 e.keyCode == 8) // Backspace. 400 ) { 401 return; 402 } 403 404 if (this.updateDelay) { 405 clearTimeout(this.updateDelay); 406 } 407 408 this.updateDelay = setTimeout(function(){ 409 self.updateOptionElements(); 410 self.updateManualElements(); 411 }, 500); 412 }; 413 414 /** 415 * Given an object of options, convert it to a text string. 416 */ 417 Drupal.optionsElement.prototype.optionsToText = function() { 418 var $rows = $('tbody tr', this.optionsElement); 419 var output = ''; 420 var previousParent = $rows.filter(':last').find('input.option-parent').val(); 421 var rowCount = $rows.size(); 422 var defaultValues = []; 423 424 // Loop through rows in reverse to find parents easier. 425 for (var rowIndex = rowCount - 1; rowIndex >= 0; rowIndex--) { 426 var indent = $rows.eq(rowIndex).find('input.option-depth').val(); 427 var key = $rows.eq(rowIndex).find('input.option-key').val(); 428 var value = $rows.eq(rowIndex).find('input.option-value').val(); 429 var parent = $rows.eq(rowIndex).find('input.option-parent').val(); 430 var checked = $rows.eq(rowIndex).find('input.option-default').attr('checked'); 431 432 // Add to default values. 433 if (checked) { 434 defaultValues.push(key); 435 } 436 437 // Handle groups. 438 if (this.optgroups && key == previousParent) { 439 output = '<' + key + '>' + "\n" + output; 440 previousParent = ''; 441 } 442 // Typical key|value pairs. 443 else { 444 // Exit out of any groups. 445 if (this.optgroups && previousParent != parent && parent !== '') { 446 output = "<>\n" + output; 447 } 448 if (this.keyType == 'none' || this.keyType == 'associative') { 449 output = value + "\n" + output; 450 previousParent = parent; 451 } 452 else if (key == '' && value == '') { 453 output = "\n" + output; 454 } 455 else { 456 output = key + '|' + value + "\n" + output; 457 previousParent = parent; 458 } 459 } 460 } 461 462 this.manualOptionsElement.value = output; 463 this.manualDefaultValueElement.value = defaultValues.join(', '); 464 }; 465 466 /** 467 * Given a text string, convert it to an object. 468 */ 469 Drupal.optionsElement.prototype.optionsFromText = function() { 470 var rows = this.manualOptionsElement.value.match(/^.*$/mg); 471 var parentKey = ''; 472 var options = []; 473 var defaultValues = {}; 474 475 // Drop the last row if empty. 476 if (rows.length && rows[rows.length - 1] == '') { 477 rows.pop(); 478 } 479 480 if (this.multiple) { 481 var defaults = this.manualDefaultValueElement.value.split(','); 482 for (var n in defaults) { 483 var defaultValue = defaults[n].replace(/^[ ]*(.*?)[ ]*$/, '$1'); // trim(). 484 defaultValues[defaultValue] = defaultValue; 485 } 486 } 487 else { 488 var defaultValue = this.manualDefaultValueElement.value.replace(/^[ ]*(.*?)[ ]*$/, '$1'); // trim(). 489 defaultValues[defaultValue] = defaultValue; 490 } 491 492 for (var n in rows) { 493 var row = rows[n].replace(/^[ ]*(.*?)[ ]*$/, '$1'); // trim(). 494 var key = ''; 495 var value = ''; 496 var checked = false; 497 var hasChildren = false; 498 var groupClear = false; 499 500 var matches = {}; 501 // Row is a group. 502 if (this.optgroups && (matches = row.match(/^\<([^>]*)\>$/))) { 503 if (matches[0] == '<>') { 504 parentKey = ''; 505 groupClear = true; 506 } 507 else { 508 parentKey = key = value = matches[1]; 509 hasChildren = true; 510 } 511 } 512 // Row is a key|value pair. 513 else if ((this.keyType == 'numeric' || this.keyType == 'custom') && (matches = row.match(/^([^|]+)\|(.*)$/))) { 514 key = matches[1]; 515 value = matches[2]; 516 } 517 // Row is a straight value. 518 else { 519 key = this.keyType == 'numeric' ? '' : row; 520 value = row; 521 } 522 523 if (!groupClear) { 524 options.push({ 525 key: key, 526 value: value, 527 parent: (key !== parentKey ? parentKey : ''), 528 hasChildren: hasChildren, 529 checked: (defaultValues[key] ? 'checked' : false) 530 }); 531 } 532 } 533 534 // Convert options to numeric if no key is specified. 535 if (this.keyType == 'numeric') { 536 var nextKey = this.nextNumericKey(); 537 for (var n in options) { 538 if (options[n].key == '') { 539 options[n].key = nextKey; 540 nextKey++; 541 } 542 } 543 } 544 545 return options; 546 } 547 548 /** 549 * Utility method to get the next numeric option in a list of options. 550 */ 551 Drupal.optionsElement.prototype.nextNumericKey = function(options) { 552 this.keyType = 'custom'; 553 options = this.optionsFromText(); 554 this.keyType = 'numeric'; 555 556 var maxKey = -1; 557 for (var n in options) { 558 if (options[n].key.match(/^[0-9]+$/)) { 559 maxKey = Math.max(maxKey, options[n].key); 560 } 561 } 562 return maxKey + 1; 563 } 564 565 /** 566 * Theme function for creating a new options element. 567 * 568 * @param optionsElement 569 * An options element object. 570 */ 571 Drupal.theme.prototype.optionsElement = function(optionsElement) { 572 var output = ''; 573 var options = optionsElement.optionsFromText(); 574 var defaultType = optionsElement.multiple ? 'checkbox' : 'radio'; 575 var keyType = optionsElement.keyType == 'custom' ? 'textfield' : 'hidden'; 576 577 // Helper function to print out a single draggable option row. 578 function tableDragRow(key, value, parentKey, indent, status) { 579 var output = ''; 580 output += '<tr class="draggable' + (indent > 0 ? ' indented' : '') + '">' 581 output += '<td class="option-default-cell">'; 582 for (var n = 0; n < indent; n++) { 583 output += Drupal.theme('tableDragIndentation'); 584 } 585 output += '<input type="hidden" class="option-parent" value="' + parentKey + '" />'; 586 output += '<input type="hidden" class="option-depth" value="' + indent + '" />'; 587 output += '<input type="' + defaultType + '" name="' + optionsElement.identifier + '-default" class="form-radio option-default" value="' + key + '"' + (status == 'checked' ? ' checked="checked"' : '') + (status == 'disabled' ? ' disabled="disabled"' : '') + ' />'; 588 output += '</td><td class="' + (keyType == 'textfield' ? 'option-key-cell' : 'option-value-cell') +'">'; 589 output += '<input type="' + keyType + '" class="' + (keyType == 'textfield' ? 'form-text ' : '') + 'option-key" value="' + key + '" />'; 590 output += keyType == 'textfield' ? '</td><td class="option-value-call">' : ''; 591 output += '<input class="form-text option-value" type="text" value="' + value + '" />'; 592 output += '</td><td class="option-actions-cell">' 593 output += '<a class="add" title="' + Drupal.t('Add new option') + '" href="#"' + (status == 'disabled' ? ' style="display: none"' : '') + '><span class="add">' + Drupal.t('Add') + '</span></a>'; 594 output += '<a class="remove" title="' + Drupal.t('Remove option') + '" href="#"' + (status == 'disabled' ? ' style="display: none"' : '') + '><span class="remove">' + Drupal.t('Remove') + '</span></a>'; 595 output += '</td>'; 596 output += '</tr>'; 597 return output; 598 } 599 600 output += '<div class="options-widget">'; 601 output += '<table id="' + optionsElement.identifier + '">'; 602 603 output += '<thead><tr>'; 604 output += '<th>' + Drupal.t('Default') + '</th>'; 605 output += keyType == 'textfield' ? '<th>' + Drupal.t('Key') + '</th>' : ''; 606 output += '<th>' + Drupal.t('Value') + '</th>'; 607 output += '<th> </th>'; 608 output += '</tr></thead>'; 609 610 output += '<tbody>'; 611 612 for (var n in options) { 613 var option = options[n]; 614 var depth = option.parent === '' ? 0 : 1; 615 if (option.hasChildren) { 616 output += tableDragRow(option.key, option.key, option.parent, depth, 'disabled'); 617 } 618 else { 619 output += tableDragRow(option.key, option.value, option.parent, depth, option.checked); 620 } 621 } 622 623 output += '</tbody>'; 624 output += '</table>'; 625 output += '<div>'; 626 627 return output; 628 } 629 630 Drupal.theme.prototype.optionsElementToggle = function() { 631 return '<div class="form-options-manual"><a href="#">' + Drupal.t('Manual entry') + '</a></div>'; 632 } 633 634 Drupal.theme.tableDragChangedMarker = function () { 635 return ' '; 636 }; 637 638 Drupal.theme.tableDragChangedWarning = function() { 639 return ' '; 640 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated: Thu Mar 24 11:18:33 2011 | Cross-referenced by PHPXref 0.7 |