home *** CD-ROM | disk | FTP | other *** search
/ Freelog 100 / FreelogNo100-NovembreDecembre2010.iso / Graphisme / GoogleSketchUp / GoogleSketchUpWFR.exe / GoogleSketchUp8.msi / SketchUpMeta.cab / configurator.js.78D17A5F_0E0A_44D2_877D_2C56D45D16B7 < prev    next >
Encoding:
Text File  |  2010-08-26  |  23.8 KB  |  812 lines

  1. //  Copyright: Copyright 2008 Google Inc.
  2. //  License: All Rights Reserved.
  3.  
  4. /**
  5.  * @fileoverview Configuration panel support routines. NOTE that this file
  6.  * relies on the dcbridge.js file having been included as a prerequisite as
  7.  * well as the components.js base routines common to all component dialogs.
  8.  */
  9.  
  10. /**
  11.  * Configurator object used as a namespace.
  12.  * @type {Object}
  13.  */
  14. var cfg = {};
  15.  
  16. // Export the su namespace. See dcbridge.js for definition.
  17. var su = window.su;
  18.  
  19. // Export the skp namespace. See dcbridge.js for definition.
  20. var skp = window.skp;
  21.  
  22. // Export conv namespace. See converter.js for definition.
  23. var conv = window.conv;
  24.  
  25. // Export comp namespace. See components.js for definition.
  26. var comp = window.comp;
  27.  
  28. // Export the $ function. See dcbridge.js for definition.
  29. var $ = window.$;
  30.  
  31. /**
  32.  * Object used to store a list of attribute values that are changed as the
  33.  * user chooses each options.
  34.  * @type {Object}
  35.  */
  36. cfg.changedValues = {};
  37.  
  38. /**
  39.  * Container for the root entity being configured.
  40.  * @type {null}
  41.  */
  42. cfg.rootEntity = null;
  43.  
  44. /**
  45.  * Placeholder for last custom style sheet link, used to assist with removal.
  46.  * @type {null}
  47.  * @private
  48.  */
  49. cfg.lastCustomCSS_ = null;
  50.  
  51. /**
  52.  * A limit after which we show a confirm dialog rather than trying to merge
  53.  * attributes directly. the goal here is to avoid having select-all or
  54.  * fence operations which select large numbers of elements from triggering
  55.  * slowdowns due to the config panel trying to merge big attribute sets.
  56.  * @type {number}
  57.  */
  58. cfg.CONFIRM_SIZE = 50;
  59.  
  60. /**
  61.  * How many decimal places to show when the user is editing a configure value.
  62.  * @type {number}
  63.  */
  64. cfg.DEFAULT_FORMAT_DECIMAL_PLACES = 3;
  65.  
  66. /**
  67.  * Initializes the configuration panel with content from the current
  68.  * selection.
  69.  */
  70. cfg.init = function() {
  71.  
  72.   // Store the loaded HTML so we can reset it on refresh.
  73.   cfg.originalHTML = $('original-html').value;
  74.  
  75.   su.callRuby('pull_information',
  76.     {'onsuccess': 'su.handlePullInformationSuccess',
  77.     'oncomplete': 'cfg.initRootEntity'});
  78.  
  79. };
  80.  
  81. /**
  82.  * Initializes the root entity data and updates the user interface as a
  83.  * downstream activity, ensuring the content of the configuration panel is
  84.  * current with the root entity data found.
  85.  * @param {string} queryid The unique ID of the invocation that triggered
  86.  *     this callback.
  87.  */
  88. cfg.initRootEntity = function(queryid) {
  89.  
  90.   cfg.ZERO_ENTITIES_MESSAGE = '<div class="no-selection-head">' +
  91.     su.translateString('No Components Selected') + '</div>' +
  92.     '<div class="no-selection-content">' +
  93.     su.translateString(
  94.         'Select one or more components to view their options.') +
  95.     '</div>';
  96.  
  97.   cfg.ZERO_OPTIONS_MESSAGE =
  98.     su.translateString('There are no options to choose on this component.');
  99.  
  100.   cfg.NO_MATCHING_MESSAGE =
  101.     su.translateString('There are no matching options to choose ' +
  102.     'across this selection.');
  103.  
  104.   comp.pullSelectionIds(
  105.     {'oncomplete': 'cfg.handlePullSelectionIdsComplete'});
  106. };
  107.  
  108. /**
  109.  * Initializes the user interface, relying on data in the cfg.rootEntity
  110.  * object to provide content values.
  111.  */
  112. cfg.initUI = function() {
  113.   var root;
  114.   var value;
  115.   var units;
  116.   var arr;
  117.   var totalFields;
  118.   var attrs;
  119.   var name;
  120.   var img;
  121.   var formLabel;
  122.   var hasFoundValue;
  123.   var selectedString;
  124.   var optarr;
  125.   var width;
  126.   var height;
  127.   var filePath;
  128.   var localPath;
  129.  
  130.   // Keep ESCAPE from closing the panel. ENTER applies any changes.
  131.   comp.installKeyHandler('down', function(evt) {
  132.     var keycode = su.getKeyCode(evt);
  133.     if (keycode == su.ESCAPE_KEY) {
  134.       su.preventDefault(evt);
  135.     } else if (keycode == su.ENTER_KEY) {
  136.       var applyButton = $('applyButton');
  137.       if (su.isValid(applyButton)) {
  138.         if (applyButton.disabled == false) {
  139.           cfg.doApply();
  140.         }
  141.       }
  142.     }
  143.   });
  144.  
  145.   cfg.clearCustomStyle();
  146.  
  147.   root = cfg.rootEntity;
  148.   if (su.notValid(root)) {
  149.     su.setContent(document.body, cfg.ZERO_ENTITIES_MESSAGE);
  150.     return;
  151.   }
  152.  
  153.   su.setContent(document.body, cfg.originalHTML);
  154.  
  155.   arr = [];
  156.  
  157.   // If our root object contains a file variable then strip the
  158.   // filename off of its end to arrive at an absolute, local path to
  159.   // where we will look for CSS or image content.
  160.   if (su.isValid(root.file)) {
  161.     filePath = su.unescapeHTML(root.file + '');
  162.     filePath = filePath.replace(/\\/gi, '/');
  163.     localPath = filePath.substring(0, filePath.lastIndexOf('/') + 1);
  164.   } else {
  165.     localPath = '';
  166.   }
  167.  
  168.   // Translate the submit button.
  169.   $('applyButton').value = su.translateString('Apply');
  170.  
  171.   // Handle top heading (name). Show the count message as the default.
  172.   value = root.name || su.translateString('Unnamed Component');
  173.   su.setContent('config-head', comp.formatContent(value));
  174.  
  175.   // Handle subheading (summary).
  176.   value = comp.getAttributeValue(root, 'summary') || '';
  177.   if (su.isEmpty(value) == false) {
  178.     su.setContent('config-subhead', comp.formatContent(value));
  179.     su.show('config-subhead')
  180.   } else {
  181.     su.hide('config-subhead')
  182.   }
  183.  
  184.   value = comp.getAttributeValue(root, 'msrp');
  185.   if (su.isEmpty(value) == false) {
  186.     units = comp.getAttributeUnits(root, 'msrp') || 'DOLLARS';
  187.     value = conv.format(conv.fromBase(value, units), units,
  188.       cfg.DEFAULT_FORMAT_DECIMAL_PLACES, true, skp.decimalDelimiter());
  189.     if (cfg.$single == false) {
  190.       value += ' ' + su.translateString('total');
  191.     }
  192.     su.setContent('config-msrp', comp.formatContent(value));
  193.     su.show('config-msrp')
  194.   } else {
  195.     su.hide('config-msrp')
  196.   }
  197.  
  198.   value = comp.getAttributeValue(root, 'description') || '';
  199.   if (su.isEmpty(value) == false) {
  200.     su.setContent('config-description', comp.formatContent(value));
  201.     su.show('config-description')
  202.   } else {
  203.     su.hide('config-description')
  204.   }
  205.  
  206.   value = comp.getAttributeValue(root, 'creator') || '';
  207.   if (su.isEmpty(value) == false) {
  208.     su.setContent('config-creator', su.translateString('by ') +
  209.       comp.formatContent(value));
  210.     su.show('config-creator')
  211.   } else {
  212.     su.hide('config-creator')
  213.   }
  214.  
  215.   value = comp.getAttributeValue(root, 'itemcode') || '';
  216.   if (su.isEmpty(value) == false) {
  217.     su.setContent('config-itemcode', comp.formatContent(value));
  218.     su.show('config-itemcode')
  219.   } else {
  220.     su.hide('config-itemcode')
  221.   }
  222.  
  223.   value = comp.getAttributeValue(root, 'imageurl');
  224.   if (su.notEmpty(value)) {
  225.     // If the image path is just a filename (no folders) then append the
  226.     // local skp path to try for a local load.
  227.     if (value.indexOf('\\') == -1 && value.indexOf('/') == -1) {
  228.       value = localPath + value;
  229.     }
  230.   } else {
  231.     value = '../../../plugins/config-thumb.jpg?' + Math.random();
  232.   }
  233.  
  234.   arr.length = 0;
  235.   arr.push('<img id="thumbnail" src="', value, '" alt="',
  236.     su.translateString('Component'), '" class="config-thumb');
  237.   if (cfg.$single == false) {
  238.     arr.push('-multiselect');
  239.   }
  240.   arr.push('"/>');
  241.   su.setContent('config-image', arr.join(''));
  242.  
  243.   // This checks to see if the image loads. If there is a load error, then
  244.   // we will set the path of our image to the default thumb path.
  245.   img = new Image;
  246.   img.onerror = function() {
  247.       $('thumbnail').src = '../../../plugins/config-thumb.jpg?' +
  248.         Math.random();
  249.     };
  250.   img.src = value;
  251.  
  252.   // Handle attribute table.
  253.   totalFields = 0;
  254.   arr.length = 0;
  255.   arr.push('<table cellspacing="0"><tbody>');
  256.  
  257.   attrs = root.attributeDictionaries[comp.DICTIONARY];
  258.  
  259.   // Note that attrs is an Object, not an array, so using for (name in attrs)
  260.   // is okay in this case.
  261.   for (name in attrs) {
  262.  
  263.     // Do not show attributes that start with an underscore,
  264.     // these are used internally for maintaining UI state.
  265.     var attr = comp.getAttribute(root, name);
  266.  
  267.     // Material is always forced to a unit type of 'STRING' to ensure
  268.     // compatibility with older component versions.
  269.     if (name.toLowerCase() == 'material') {
  270.       attr.units = 'STRING';
  271.     }
  272.  
  273.     if (name.indexOf('_') != 0 &&
  274.         attr.access != 'NONE' &&
  275.         attr.access != undefined) {
  276.  
  277.       formLabel = su.sanitizeHTML(su.ifEmpty(attr.formlabel, attr.label));
  278.  
  279.       arr.push('<tr>',
  280.         '<td class="config-label"><nobr>', formLabel, '</nobr></td>',
  281.         '<td class="config-cell">');
  282.  
  283.       value = su.sanitizeHTML(attr.value);
  284.       value = su.escapeHTML(value);
  285.  
  286.       if (attr.access != 'LIST') {
  287.         var units = su.ifEmpty(attr.units, 'STRING');
  288.         if (units == 'DEFAULT') {
  289.           units = skp.units();
  290.         }
  291.         value = conv.fromBase(attr.value, units);
  292.         value = conv.format(value + '', units,
  293.           cfg.DEFAULT_FORMAT_DECIMAL_PLACES, true, skp.decimalDelimiter());
  294.       }
  295.  
  296.       if (attr.access == 'VIEW') {
  297.         totalFields++;
  298.         arr.push('<input type="text" class="config-field-readonly" value="',
  299.           value, '" readonly="readonly" />');
  300.       } else if (attr.access == 'TEXTBOX') {
  301.         totalFields++;
  302.         arr.push('<input type="text" class="config-field" value="',
  303.           value,
  304.           '" onkeypress="$(\'applyButton\').disabled=false;" ',
  305.           'onblur="cfg.doStoreChanges(', root.id, ',\'', name,
  306.           '\',this,\'', attr.units, '\')" name="', name, '"/>');
  307.       } else if (attr.access == 'LIST') {
  308.         totalFields++;
  309.         arr.push('<select class="config-field" value="', value,
  310.           '" onkeypress="$(\'applyButton\').disabled=false;" ',
  311.           'onchange="cfg.doStoreChanges(', root.id, ',\'', name,
  312.           '\',this,\'', attr.units, '\')" name="', name, '">');
  313.  
  314.         var options = su.ifEmpty(attr.options, '');
  315.         if (su.isString(options)) {
  316.           options = su.unescapeHTML(options);
  317.         }
  318.         var valuePairs = options.split('&');
  319.         hasFoundValue = false;
  320.  
  321.         optarr = [];
  322.         for (var valuePairID = 0; valuePairID < valuePairs.length;
  323.             valuePairID++) {
  324.           var valuePair = valuePairs[valuePairID];
  325.  
  326.           if (su.notEmpty(valuePair)) {
  327.             var nameValueArray = valuePair.split('=');
  328.             selectedString = '';
  329.             value = nameValueArray[1].toLowerCase();
  330.             value = unescape(su.escapeHTML(value));
  331.             if (conv.isEqual(attr.value.toLowerCase(), value) ||
  332.               ('=' + attr.formula).toLowerCase() == value ||
  333.               ('="' + attr.value + '"').toLowerCase() == value) {
  334.               selectedString = ' selected="selected" ';
  335.               hasFoundValue = true;
  336.             }
  337.             value = unescape(nameValueArray[1]);
  338.             value = su.escapeHTML(value);
  339.             optarr.push('<option value="',
  340.               value,
  341.               '" ', selectedString, '>', unescape(nameValueArray[0]),
  342.               '</option>');
  343.           }
  344.         }
  345.  
  346.         if (hasFoundValue == false) {
  347.           optarr.unshift('<option value="', value, '"></option>');
  348.         }
  349.  
  350.         arr.push(optarr.join(''));
  351.         arr.push('</select>');
  352.       }
  353.  
  354.       arr.push('</td></tr>');
  355.     }
  356.   }
  357.   arr.push('</tbody></table>');
  358.  
  359.   if (totalFields > 0) {
  360.     su.show('config-options');
  361.     su.setContent('config-error', '')
  362.   } else {
  363.     su.hide('config-options');
  364.     if (cfg.$single == false) {
  365.       su.setContent('config-error', cfg.NO_MATCHING_MESSAGE)
  366.     } else {
  367.       su.setContent('config-error', cfg.ZERO_OPTIONS_MESSAGE)
  368.     }
  369.   }
  370.  
  371.   su.setContent('config-options', arr.join(''));
  372.  
  373.   document.getElementById('content').style.top =
  374.     su.elementHeight(document.getElementById('header')) + 'px';
  375.  
  376.   // Resize our window to the author's specifications, or default to a
  377.   // standard size if none is provided.
  378.   width = su.ifEmpty(comp.getAttributeValue(root, 'dialogwidth'), 345);
  379.   height = su.ifEmpty(comp.getAttributeValue(root, 'dialogheight'), 560);
  380.   su.callRuby('set_dialog_properties', {'width': width, 'height': height});
  381.  
  382.   cfg.updateLayout();
  383. };
  384.  
  385. /**
  386.  * Responds to requests (usually initiated via the UI) to apply any changes
  387.  * made to the configuration panel so they appear in SketchUp.
  388.  */
  389. cfg.doApply = function() {
  390.   var key;
  391.   var parts;
  392.   var entityID;
  393.   var attribute;
  394.   var value;
  395.   var noRedraw;
  396.   var attributeCount = 0;
  397.   var changes;
  398.   var elem;
  399.   var name;
  400.  
  401.   // Current field isn't always picked up if the user didn't tab out but
  402.   // instead just hit return...so grab its value.
  403.   if (su.isValid(elem = document.activeElement)) {
  404.     name = elem.getAttribute('id') || elem.getAttribute('name');
  405.     if (su.notEmpty(name) && (name != 'applyButton')) {
  406.       // Force the blur of the currently selected element to ensure that
  407.       // storeChanges for that value is fired.
  408.       elem.blur();
  409.     }
  410.   }
  411.  
  412.   // If the changed value list doesn't have values then we can simply return.
  413.   if (su.isEmpty(su.getKeys(cfg.changedValues))) {
  414.     return;
  415.   }
  416.  
  417.   // The changed values hash can carry the data to be serialized for
  418.   // transmission over the bridge, we just need to include the entities.
  419.   comp.pushAttributeSet(cfg.entityIds, cfg.changedValues);
  420.   cfg.changedValues = {};
  421.  
  422. };
  423.  
  424. /**
  425.  * Responds to requests (usually initiated via the UI) to cancel any changes
  426.  * made to the configuration values and close the dialog window.
  427.  */
  428. cfg.doCancel = function() {
  429.   su.callRuby('do_close');
  430. };
  431.  
  432. /**
  433.  * Stores a changed value in the configuration panels set of changes so they
  434.  * can be pushed to SketchUp for display at the appropriate time.
  435.  * @param {string} nodeID The ID of the element being updated.
  436.  * @param {string} attribute The name of the attribute to modify.
  437.  * @param {element} field The field whose value is being committed.
  438.  * @return {boolean} True to allow default event handling to continue.
  439.  */
  440. cfg.doStoreChanges = function(nodeID, attribute, field) {
  441.   var defaultValue;
  442.   var displayValue;
  443.   var div;
  444.   var baseValue;
  445.  
  446.   // Get the entity and unit.
  447.   var entity = su.findEntity(nodeID, cfg.rootEntity);
  448.   var units = su.ifEmpty(comp.getAttributeUnits(entity, attribute), 'STRING');
  449.   if (units == 'DEFAULT') {
  450.     units = skp.units();
  451.   }
  452.  
  453.   // If this is a list box, then store the value of the field. Otherwise, it
  454.   // must be a text box, so scrub whatever was entered into a valid value.
  455.   if (su.isValid(field.selectedIndex)) {
  456.     baseValue = field.value;
  457.   } else {
  458.     // Take the entered value and turn it into the appropriate base 
  459.     // units. (For example, lengths are always stored in inches, regardless
  460.     // of the unit they are displayed in.)
  461.     var enteredValue = conv.parseTo(field.value, units, skp.decimalDelimiter());
  462.     baseValue = conv.toBase(enteredValue, units);
  463.   }
  464.  
  465.   cfg.changedValues[nodeID + '__' + attribute] = baseValue + '';
  466.   // If it's a text box we're displaying, format the string.
  467.   if (field.type == 'text') {
  468.     displayValue = conv.format(conv.fromBase(baseValue, units), units,
  469.       cfg.DEFAULT_FORMAT_DECIMAL_PLACES, true, skp.decimalDelimiter());
  470.     // Using this innerHTML trick unescapes the &FFF; style unicode
  471.     // characters so they display properly in a form field.
  472.     div = document.createElement('div');
  473.     div.innerHTML = displayValue;
  474.     displayValue = div.innerHTML;
  475.  
  476.     field.value = displayValue;
  477.   }
  478.  
  479.   $('applyButton').disabled = false;
  480.   return true;
  481. };
  482.  
  483. /**
  484.  * Returns an Array of key paths, dot-separated key names which represent
  485.  * the paths to descendant objects in the object provided. For example, a
  486.  * nested object {a: {b: 'foo'}} would return ['a', 'a.b'] for key paths.
  487.  * @param {object} anObject The object to recursively iterate.
  488.  * @param {String} opt_prefix A prefix, passed from the prior invocation
  489.  *     internally. to maintain the key string. Do not pass this value
  490.  *     yourself.
  491.  * @return {Array} The list of key paths for anObject.
  492.  */
  493. cfg.$getKeyPaths = function(anObject, opt_prefix) {
  494.   var arr;
  495.   var i;
  496.   var len;
  497.   var key;
  498.   var slot;
  499.  
  500.   arr = [];
  501.   if (su.isScalar(anObject)) {
  502.     return arr;
  503.   } else if (su.isJSArray(anObject)) {
  504.     // For arrays we want keys to preserve [i] as part of the path so we can
  505.     // look back and realize we had an array in the data structure.
  506.     len = anObject.length;
  507.     for (i = 0; i < len; i++) {
  508.       key = opt_prefix ? opt_prefix + '[' + i + ']' : '[' + i + ']';
  509.       arr.push(key);
  510.       slot = anObject[i];
  511.       if ((slot != null) && (slot.constructor === Object)) {
  512.         arr = arr.concat(cfg.$getKeyPaths(slot, key));
  513.       }
  514.     }
  515.     return arr;
  516.   } else {
  517.     for (i in anObject) {
  518.       key = opt_prefix ? opt_prefix + '.' + i : i;
  519.       arr.push(key);
  520.       slot = anObject[i];
  521.       if ((slot != null) && (slot.constructor === Object)) {
  522.         arr = arr.concat(cfg.$getKeyPaths(slot, key));
  523.       }
  524.     }
  525.     return arr;
  526.   }
  527. };
  528.  
  529. /**
  530.  * Responds to notifications that the configuration panel is being resized.
  531.  */
  532. cfg.handleResize = function() {
  533.   cfg.updateLayout();
  534. };
  535.  
  536. /**
  537.  * Updates the layout of the panel. Note that this is only actively used by IE
  538.  * as Safari's CSS engine can manage the interface automatically.
  539.  */
  540. cfg.updateLayout = function() {
  541.   var elem;
  542.  
  543.   if (su.IS_MAC) {
  544.     return;
  545.   }
  546.  
  547.   elem = $('content');
  548.   if (su.isValid(elem)) {
  549.     try {
  550.       elem.style.height = (su.elementGetBorderBox('background').height -
  551.           su.elementGetBorderBox('header').height - 
  552.           su.elementGetBorderBox('footer').height) + 'px';
  553.     } catch (e) {
  554.       // Ignore when new value(s) aren't viable.
  555.     }
  556.   }
  557. };
  558.  
  559. /**
  560.  * Handles success notification from the Ruby pull_attribute_tree function
  561.  * and triggers initial UI construction based on the selection attribute
  562.  * data provided by that routine.
  563.  * @param {string} queryid The unique ID of the invocation that triggered
  564.  *     this callback.
  565.  */
  566. cfg.handlePullAttributesComplete = function(queryid) {
  567.   var obj;
  568.   var arr;
  569.   var len;
  570.   var count;
  571.   var keys;
  572.   var key;
  573.   var i;
  574.   var j;
  575.   var len2;
  576.   var dict;
  577.   var items;
  578.   var item;
  579.   var root;
  580.   var last;
  581.   var source;
  582.   var attrs;
  583.   var attr;
  584.   var name;
  585.   var msrp;
  586.  
  587.   // By default we clear any changed attribute list. This is updated for
  588.   // multiple selection to auto-dirty shared values.
  589.   cfg.changedValues = {};
  590.  
  591.   // Keep a running total of the cost of the selection.
  592.   msrp = 0;
  593.  
  594.   if (su.notValid(obj = su.getRubyResponse(queryid))) {
  595.     alert(su.translateString('No attribute data returned.'));
  596.   }
  597.  
  598.   if (su.notValid(arr = obj['entities'])) {
  599.     alert(su.translateString('No entity data returned.'));
  600.   }
  601.  
  602.   len = arr.length;
  603.   cfg.$count = len;
  604.  
  605.   switch (len) {
  606.   case 0:
  607.     // Empty selection, nothing to configure but we'll want to redraw.
  608.     cfg.$single = null;
  609.     cfg.rootEntity = null;
  610.     break;
  611.   case 1:
  612.     // Single selection, most common case.
  613.     cfg.$single = true;
  614.     cfg.rootEntity = arr[0];
  615.  
  616.     // Place this component's name attribute into the root for display.
  617.     cfg.rootEntity.name = su.ifEmpty(comp.getAttributeValue(cfg.rootEntity,
  618.         'name'), cfg.rootEntity.name);
  619.     break;
  620.   default:
  621.     // Multiple-selection. have to merge attributes into a common root
  622.     // entity that will allow the user to edit a group en-masse.
  623.     cfg.$single = false;
  624.     count = 0;
  625.     dict = {};
  626.  
  627.     // Get a list of keys for each object that represent the set of nested
  628.     // object names which might be shared. 
  629.     for (i = 0; i < len; i++) {
  630.  
  631.       obj = arr[i];
  632.       // Note that we don't bother with objects that aren't components with
  633.       // dynamic attribute subcontent.
  634.       if (su.notValid(obj) || (obj.typename != 'ComponentInstance')) {
  635.         continue;
  636.       }
  637.       if (su.notValid(attrs = comp.getAttributes(obj))) {
  638.         continue;
  639.       }
  640.  
  641.       msrp += parseFloat(su.ifEmpty(comp.getAttributeValue(obj, 'msrp'), 0));
  642.  
  643.       // Count the valid ones for later filtering.
  644.       count++;
  645.       keys = cfg.$getKeyPaths(obj);
  646.  
  647.       // Inject the keys/counts into our dictionary of known keys.
  648.       len2 = keys.length;
  649.       for (j = 0; j < len2; j++) {
  650.         key = keys[j];
  651.         dict[key] = (dict[key] || 0) + 1;
  652.       }
  653.     }
  654.  
  655.     // Now we remove those that are shared, leaving the list we should
  656.     // prune from a prototypical instance.
  657.     items = su.getItems(dict);
  658.     len = items.length;
  659.     for (i = 0; i < len; i++) {
  660.       item = items[i];
  661.       if (item[1] == count) {
  662.         try {
  663.           delete dict[item[0]];
  664.         } catch (e) {
  665.           // Ignore errors.
  666.         }
  667.       }
  668.     }
  669.  
  670.     // Sort the remaining keys so shortest go first, which ensures that
  671.     // we remove from the top down in the next loop.
  672.     keys = su.getKeys(dict);
  673.     keys.sort(function(a, b) {
  674.       if (a.length < b.length) {
  675.         return -1;
  676.       } else if (a.length == b.length) {
  677.         if (a < b) {
  678.           return -1;
  679.         } else if (a == b) {
  680.           return 0;
  681.         } else {
  682.           return 1;
  683.         };
  684.       } else {
  685.         return 1;
  686.       }
  687.     });
  688.  
  689.     // The optimization here is that we only need to remove the top-most
  690.     // slot for each key, so a set of keys which all start with 'x.' can be
  691.     // simplified to 'x'.
  692.     len = keys.length;
  693.     for (i = 0; i < len; i++) {
  694.         key = keys[i];
  695.         if ((last != null) && (key.indexOf(last) == 0)) {
  696.           keys[i] = null;
  697.         };
  698.         last = key;
  699.     };
  700.  
  701.     // We'll use the first selected object as the prototype.
  702.     root = arr[0];
  703.  
  704.     len = keys.length;
  705.     for (i = 0; i < len; i++) {
  706.       key = keys[i];
  707.       if (key == null) {
  708.         continue;
  709.       };
  710.       try {
  711.         source = 'delete root.' + key;
  712.         eval(source);
  713.       } catch (e) {
  714.         // Ignore errors.
  715.       }
  716.     }
  717.  
  718.     cfg.rootEntity = root;
  719.     root.name = cfg.$count + ' ' + su.translateString('Components');
  720.  
  721.     // Mapping root entity leaves to the changedValues data effectively
  722.     // dirties the entire set of shared attributes so we can immediately
  723.     // apply changes to all selected items without having to edit each one.
  724.     cfg.changedValues = {};
  725.     attrs = root.attributeDictionaries[comp.DICTIONARY];
  726.  
  727.     // Note that attrs is an Object, not an array, so using for/in looping
  728.     // is okay in this case.
  729.     for (name in attrs) {
  730.       attr = comp.getAttribute(root, name);
  731.       if (name.indexOf('_') != 0 &&
  732.           attr.access != 'NONE' &&
  733.           attr.access != undefined) {
  734.           cfg.changedValues[name] = attr.value;
  735.       }
  736.     }
  737.  
  738.     comp.setAttributeValue(root, 'msrp', msrp);
  739.     break;
  740.   }
  741.  
  742.   cfg.initUI();
  743. };
  744.  
  745. /**
  746.  * Responds to notification that the pullSelectionIds call has succeeded on
  747.  * our behalf. Changes to the current selection will trigger this routine so
  748.  * that the configuration panel remains slaved to the current selection.
  749.  * @param {string} queryid The unique ID of the invocation that triggered
  750.  *     this callback.
  751.  * @param {string} idlist An optional comma-delimited list of specific
  752.  *     IDs to process as the selection set.
  753.  */
  754. cfg.handlePullSelectionIdsComplete = function(queryid, idlist) {
  755.   var ids;
  756.   var list;
  757.   var len;
  758.  
  759.   // NOTE that we don't leave these purely on the comp object since we
  760.   // share that dataset with the manager panel and it may be altering our
  761.   // data if it changes focus.
  762.   ids = idlist || comp.selectionIds;
  763.  
  764.   // ID is no selection ID? Clear all cached ID sets.
  765.   if (ids == -1) {
  766.     ids = '';
  767.     comp.selectionIds = null;
  768.     cfg.entityIds = null;
  769.   } else {
  770.     cfg.entityIds = ids;
  771.   }
  772.  
  773.   if (su.isEmpty(ids)) {
  774.     cfg.clearCustomStyle();
  775.     su.setContent(document.body, cfg.ZERO_ENTITIES_MESSAGE);
  776.     return;
  777.   }
  778.  
  779.   list = ids.split(',');
  780.   len = list.length;
  781.   if (len == 0) {
  782.     // No length? No selection -- 0 entities selected.
  783.     cfg.clearCustomStyle();
  784.     su.setContent(document.body, cfg.ZERO_ENTITIES_MESSAGE);
  785.     return;
  786.   } else if (len > cfg.CONFIRM_SIZE) {
  787.     // Over the limit, have to confirm and then either cancel or
  788.     // continue.
  789.     if (!confirm('Merging multiple items might be slow. Continue?')) {
  790.       cfg.clearCustomStyle();
  791.       su.setContent(document.body, len + ' entities selected.');
  792.       return;
  793.     }
  794.   }
  795.  
  796.   comp.pullAttributes({'selection_ids': ids,
  797.     'oncomplete': 'cfg.handlePullAttributesComplete'});
  798. };
  799.  
  800. /**
  801.  * Clears any custom style sheet that may be current for the configuration
  802.  * panel.
  803.  */
  804. cfg.clearCustomStyle = function() {
  805.   // Remove any custom style sheet regardless of selection size so we
  806.   // don't have leftover UI even on an empty selection.
  807.   if (su.notEmpty(cfg.lastCustomCSS_)) {
  808.     su.removeStylesheet(document, cfg.lastCustomCSS_);
  809.     cfg.lastCustomCSS_ = null;
  810.   }
  811. };
  812.