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

  1. //  Copyright: Copyright 2008 Google Inc.
  2. //  License: All Rights Reserved.
  3.  
  4. /**
  5.  * @fileoverview Manage Attributes panel support routines. NOTE that this
  6.  * file relies on the dcbridge.js file having been included as well as the
  7.  * components.js base routines common to all component dialogs.
  8.  */
  9.  
  10. /**
  11.  * The global manager namespace, containing functions, properties, and
  12.  * constants specific to the Dynamic Components Manager panel.
  13.  * @type {Object}
  14.  */
  15. var mgr = {};
  16.  
  17. // Export the su namespace. See dcbridge.js for definition.
  18. var su = window.su;
  19.  
  20. // Export the skp namespace. See dcbridge.js for definition.
  21. var skp = window.skp;
  22.  
  23. // Export comp namespace. See components.js for definition.
  24. var comp = window.comp;
  25.  
  26. // Export conv namespace. See converter.js for definition.
  27. var conv = window.conv;
  28.  
  29. // Export the $ function. See dcbridge.js for definition.
  30. var $ = window.$;
  31.  
  32. // String constants used in the user interface in various places.
  33. mgr.ENTER_NAME_STRING = 'Enter Name';
  34. mgr.FALSE_STRING = 'FALSE';
  35. mgr.TRUE_STRING = 'TRUE';
  36. mgr.ERROR_PREFIX = '<span class="subformula-error">#</span> ';
  37. mgr.APPLYING_ATTRIBUTE_MESSAGE = '<span id="applying-attribute-message">' +
  38.     'Saving...' + '</span>';
  39. mgr.DEFAULT_OPTION_LABEL = 'Enter Option Here';
  40. mgr.DEFAULT_OPTION_VALUE = 'Enter Value';
  41. mgr.ADD_ATTRIBUTE = 'Add attribute';
  42.  
  43. /**
  44.  * How many decimal places to show when the user enters edit mode.
  45.  * @type {number}
  46.  */
  47. mgr.DEFAULT_EDIT_DECIMAL_PLACES = 6;
  48.  
  49. /**
  50.  * Maximum string length we will allow a component or group to be renamed to.
  51.  * @type {number}
  52.  */
  53. mgr.MAX_NAME_LENGTH = 64;
  54.  
  55. /**
  56.  * Value that is stored in the scaletool attribute when all handles are hidden.
  57.  * @type {number}
  58.  */
  59. mgr.ALL_SCALE_HANDLES_HIDDEN = 127;
  60.  
  61. /**
  62.  * The offset used for tab display in the component attributes panel.
  63.  * @type {number}
  64.  */
  65. mgr.EDIT_FIELD_REFERENCE_TAB_HEIGHT = 15;
  66.  
  67. /**
  68.  * The standard width of the editor cell border, used for offset
  69.  * computations.
  70.  * @type {number}
  71.  */
  72. mgr.BORDER_OFFSET = 3;
  73.  
  74. /**
  75.  * The standard offset used for computations of edit field positioning.
  76.  * @type {number}
  77.  */
  78. mgr.FIELD_OFFSET = 24;
  79.  
  80. /**
  81.  * The position used when moving the editor cell offscreen for hiding.
  82.  * @type {number}
  83.  */
  84. mgr.HIDDEN_EDITOR_TOP = -5000;
  85.  
  86. /**
  87.  * The offset height necessary to ensure proper positioning of edit-panel.
  88.  * This must be adjusted if markup layout is adjusted.
  89.  * @type {number}
  90.  */
  91. mgr.HIGHLIGHT_EDIT_OFFSET = 85;
  92.  
  93. /**
  94.  * Keycode for US ASCII 101 '='.
  95.  * @type {number}
  96.  */
  97. mgr.EQUAL_KEY_STD = 61;
  98.  
  99. /**
  100.  * Keycode for US ASCII 101 '=' on number pad.
  101.  * @type {number}
  102.  */
  103. mgr.EQUAL_KEY_NUM = 187;
  104.  
  105. /**
  106.  * Keycode for US ASCII 101 '@' (commat).
  107.  * @type {number}
  108.  */
  109. mgr.COMMAT_KEY = 50;
  110.  
  111. /**
  112.  * Access enumeration defining the alternatives for user atttribute access.
  113.  * @type {Array}
  114.  */
  115. mgr.ACCESS = [
  116.   {value: 'NONE', label: 'Users cannot see this attribute.'},
  117.   {value: 'VIEW', label: 'Users can see this attribute.'},
  118.   {value: 'TEXTBOX', label: 'Users can edit as a textbox.'},
  119.   {value: 'LIST', label: 'Users can select from a list.', disabled: true}
  120. ];
  121.  
  122. /*
  123.  * A list of the cells which should be treated as having an implicit formula
  124.  * as their content.
  125.  * @type {array}
  126.  */
  127. mgr.FORMULA_CELL_LABELS = ['onClick'];
  128.  
  129. /**
  130.  * Offset to add when focusing on the 'current' field in a field list.
  131.  * @type {number}
  132.  */
  133. mgr.FOCUS_CURRENT = 0;
  134.  
  135. /**
  136.  * Offset to add when focusing on the 'next' field in a field list.
  137.  * @type {number}
  138.  */
  139. mgr.FOCUS_NEXT = 1;
  140.  
  141. /**
  142.  * Offset to add when focusing on the 'previous' field in a field list.
  143.  * @type {number}
  144.  */
  145. mgr.FOCUS_PREVIOUS = -1;
  146.  
  147. /**
  148.  * The tree instance which handles all rendering responsibilities.
  149.  */
  150. mgr.tree = null;
  151.  
  152. /**
  153.  * The last DOM Element selected via either click or mouse movement. This
  154.  * tracks the user's last selection to assist with redraw and editing.
  155.  * @type {Element}
  156.  */
  157. mgr.lastElementSelected = null;
  158.  
  159. /**
  160.  * Whether the manager is currently calling on SketchUp for Ruby data.
  161.  * @type {boolean}
  162.  */
  163. mgr._calling = false;
  164.  
  165. /**
  166.  * Whether the edit field is currently floating above the attribute tree.
  167.  * @type {boolean}
  168.  */
  169. mgr._floating = false;
  170.  
  171. /**
  172.  * The setTimeout/clearTimeout timer object used for smooth display updates.
  173.  * @type {Object}
  174.  */
  175. mgr.finalScrollAdjustTimeout = null;
  176.  
  177. /**
  178.  * The setTimeout/clearTimeout timer object used for smooth resize handling.
  179.  * @type {Object}
  180.  */
  181. mgr.finalResizeAdjustTimeout = null;
  182.  
  183. // ---
  184. // Initialization / Startup
  185. // ---
  186.  
  187. /**
  188.  * Initializes the Manage Attributes panel with content from the current
  189.  * selection.
  190.  */
  191. mgr.init = function() {
  192.  
  193.   // Set up our initial state for less screen flashing on large DCs.
  194.   mgr.updateLayout();
  195.  
  196.   mgr.SELECT_MESSAGE = '<div class="no-selection-head">' +
  197.     su.translateString('Single Component Not Selected') + '</div>' +
  198.     '<div class="no-selection-content">' +
  199.     su.translateString(
  200.         'Select a single component to view its attributes.') +
  201.     '</div>';
  202.  
  203.   // Translate static parts of the UI.
  204.   $('refresh-button').title = su.translateString('Refresh');
  205.   $('settings-button').title = su.translateString('Toggle Formula View');
  206.   $('tab-basic-title').innerHTML = su.translateString('Info');
  207.   $('tab-function-title').innerHTML = su.translateString('Functions');
  208.  
  209.   // Start off requesting common SketchUp environment information. The
  210.   // initRootEntity callback will then proceed to load entity data.
  211.   mgr.callRuby('pull_information',
  212.     {'onsuccess': 'su.handlePullInformationSuccess',
  213.     'oncomplete': 'mgr.initRootEntity'});
  214. };
  215.  
  216. /**
  217.  * Initializes the root entity data and updates the user interface as a
  218.  * downstream activity, ensuring the content of the configuration panel is
  219.  * current with the root entity data found.
  220.  * @param {string} queryid The unique ID of the invocation which triggered
  221.  *     this callback.
  222.  */
  223. mgr.initRootEntity = function(queryid) {
  224.  
  225.   // Now that we have our su.info loaded, we can calculate our help URLs.
  226.   // Note that there is a problem with loading double quotes from inside
  227.   // a translated string, so to minimize the risk of dropping a translation,
  228.   // the DC_HELP_URL is an unquoted parameter.
  229.   var intro = su.translateString('Add attributes below to create your ' +
  230.       'component options. Visit our <a href=DC_HELP_URL>getting started ' +
  231.       'guide</a> for tutorials.');
  232.   mgr.INTRO_STATUS = intro.replace(/DC_HELP_URL/gi,
  233.     'skp:do_open_url@url=' + su.info['dc_help_url']);
  234.  
  235.   mgr.FUNCTIONS_URL = su.info['dc_functions_url'];
  236.  
  237.   mgr.SELECT_MESSAGE = '<div class="no-selection-head">' +
  238.     su.translateString('Single Component Not Selected') + '</div>' +
  239.     '<div class="no-selection-content">' +
  240.     su.translateString(
  241.         'Select a single component to view its attributes.') +
  242.     '</div>';
  243.  
  244.   // Translate static parts of the UI.
  245.   $('refresh-button').title = su.translateString('Refresh');
  246.   $('settings-button').title = su.translateString('Toggle Formula View');
  247.   $('tab-basic-title').innerHTML = su.translateString('Info');
  248.   $('tab-function-title').innerHTML = su.translateString('Functions');
  249.  
  250.   // Effectively this is the same as a redraw, but without having an entity
  251.   // in place yet.
  252.   comp.pullAttributes({
  253.     'deep': true,
  254.     'oncomplete': 'mgr.handlePullAttributesComplete'
  255.   });
  256. };
  257.  
  258. /**
  259.  * Invokes a function in Ruby defined as part of the SketchUp Ruby API or as
  260.  * part of an included/required Ruby module. NOTE that this call is made in
  261.  * an asynchronous fashion. Callbacks to the JavaScript are dependent on the
  262.  * Ruby function being invoked. See SketchUp's js_callback Ruby method for
  263.  * more information on how to return results to the invoking JavaScript.
  264.  * @param {string} funcname The name of the Ruby function to invoke.
  265.  * @param {string|Object} opt_request A pre-formatted URL-style query string
  266.  *     or an object whose keys and values should be formatted into a URL
  267.  *     query string.
  268.  */
  269. mgr.callRuby = function(funcname, opt_request) {
  270.  
  271.   mgr.isCalling(true);
  272.   mgr.showCurtain();
  273.  
  274.   // Note that the call to the true bridge is last since it's async.
  275.   su.callRuby(funcname, opt_request);
  276. };
  277.  
  278. /**
  279.  * Returns the current state of the manager's "calling sketchup" flag. When a
  280.  * call to SketchUp is underway this flag will be true. You can set the value
  281.  * of the flag by passing the new value as the first parameter.
  282.  * @param {boolean} opt_flag An optional new value for the calling flag.
  283.  * @return {boolean} True when a call is actively underway.
  284.  */
  285. mgr.isCalling = function(opt_flag) {
  286.   if (su.isValid(opt_flag)) {
  287.     mgr._calling = opt_flag;
  288.   }
  289.  
  290.   return mgr._calling;
  291. };
  292.  
  293. /**
  294.  * Hides the semi-opaque event-trapping layer over the panel's document body.
  295.  */
  296. mgr.hideCurtain = function() {
  297.   su.hide('curtain');
  298. };
  299.  
  300. /**
  301.  * Shows a semi-opaque layer over the panel's document body, trapping events.
  302.  */
  303. mgr.showCurtain = function() {
  304.   var el = $('curtain');
  305.   if (su.notValid(el)) {
  306.     el = document.createElement('div');
  307.     el.setAttribute('id', 'curtain');
  308.     document.body.appendChild(el);
  309.   }
  310.  
  311.   su.show(el);
  312. };
  313.  
  314. /**
  315.  * Respond to notifications that attributes related to the managed entity have
  316.  * been retrieved.
  317.  * @param {string} queryid The unique ID of the invocation which triggered
  318.  *     this callback.
  319.  */
  320. mgr.handlePullAttributesComplete = function(queryid) {
  321.  
  322.   var obj;
  323.   var arr;
  324.  
  325.   // Depress the refresh button.
  326.   $('refresh-button').className = 'refresh-button';
  327.  
  328.   mgr.isCalling(false);
  329.   mgr.hideCurtain();
  330.  
  331.   if (su.notValid(obj = su.getRubyResponse(queryid))) {
  332.     alert(su.translateString('No attribute data returned.'));
  333.   }
  334.  
  335.   if (su.notValid(arr = obj['entities'])) {
  336.     alert(su.translateString('No entity data returned.'));
  337.   }
  338.  
  339.   if (arr.length != 1) {
  340.     // We need to hide the details panel and leave the functions tab to ensure
  341.     // that pulldown controls do not "show through" the message-panel on IE.
  342.     if (mgr.isDetailing()) {
  343.       if (mgr.tree) {
  344.         mgr.tree.hideDetailPanel();
  345.       }
  346.     }
  347.     mgr.setTab('basic');
  348.  
  349.     su.show('message-panel');
  350.     su.setContent('message-panel', mgr.SELECT_MESSAGE);
  351.     return;
  352.   } else {
  353.     su.hide('message-panel');
  354.   }
  355.  
  356.   // If the root entity has changed, meaning the user has selected another
  357.   // DC than they had before, do some cleanup of the previous state.
  358.   if (mgr.rootEntity != arr[0]) {
  359.     if (mgr.isHighlighting()) {
  360.        mgr.hideHighlight();
  361.     }
  362.   }
  363.  
  364.   // The manager (currently) works on single entity, so extract first item as
  365.   // the rootEntity driving the attribute tree.
  366.   mgr.rootEntity = arr[0];
  367.   mgr.initUI();
  368. };
  369.  
  370. /**
  371.  * Initializes the user interface of the Manage Attributes panel based on
  372.  * data in the mgr.rootEntity object acquired from the current selection.
  373.  * This operation is invoked on panel startup as well as in response to edits
  374.  * which need to push data to the Ruby side of the bridge so that the view can
  375.  * be updated in response to attribute value changes.
  376.  */
  377. mgr.initUI = function() {
  378.  
  379.   mgr.tree = new AttributeTree('mgr.tree');
  380.  
  381.   // If we have already initialized, we can skip redrawing the entire UI and
  382.   // instead redraw the mgr.tree.
  383.   if (mgr.initDone == true) {
  384.     mgr.tree.render();
  385.     return;
  386.   }
  387.   mgr.initDone = true;
  388.  
  389.   // Keep ESCAPE from closing the panel and process top-level navigation
  390.   // keys outside of any particular field (hence we don't use onkeydown).
  391.   comp.installKeyHandler('down', mgr.tree.handleKeyDown);
  392.   comp.installKeyHandler('press', mgr.tree.handleKeyPress);
  393.   comp.installKeyHandler('up', mgr.tree.handleKeyUp);
  394.  
  395.   if ($('extras').getElementsByTagName('form').length == 0) {
  396.     // Configure the extras content, which includes the editing field and the
  397.     // various affordances for showing highlighting of the currently focused
  398.     // field.
  399.     var arr = [];
  400.  
  401.     // Open a form so input elements and textareas render properly.
  402.     arr.push('<form name="edit-form" onsubmit="return false;">');
  403.  
  404.     // The editing panel and textarea used for text input.
  405.     arr.push('<div id="edit-panel" class="edit-panel">');
  406.     arr.push('<div id="edit-field-reference-tab"></div>');
  407.     arr.push('<textarea id="edit-field" name="edit-field" class="edit-field"',
  408.         ' onkeydown="', mgr.tree.id, '.handleKeyDown(this, event)"',
  409.         ' onkeypress="', mgr.tree.id, '.handleKeyPress(this, event)"',
  410.         ' onkeyup="', mgr.tree.id, '.handleKeyUp(this, event)"',
  411.         ' oncut="mgr.updateEditorLayoutTimeout()"',
  412.         ' onpaste="mgr.updateEditorLayoutTimeout()"',
  413.         '/></textarea>');
  414.     arr.push('</div>');
  415.  
  416.     // Close the form.
  417.     arr.push('</form>');
  418.  
  419.     // The focus highlighting divs.
  420.     arr.push('<div id="highlight-panel" class="highlight-panel"></div>');
  421.     arr.push('<div id="highlight-line-top" class="highlight-line"></div>');
  422.     arr.push('<div id="highlight-line-left" class="highlight-line"></div>');
  423.     arr.push('<div id="highlight-line-right" class="highlight-line"></div>');
  424.     arr.push('<div id="highlight-line-bottom" class="highlight-line"></div>');
  425.  
  426.     // The details button
  427.     arr.push('<div id="details-button" title="', su.translateString('Details'),
  428.       '" class="details-button" onclick="',
  429.       mgr.tree.id, '.showDetailPanel()"></div>');
  430.  
  431.     // The delete button.
  432.     arr.push('<div id="delete-button" title="', su.translateString('Delete'),
  433.       '" class="delete-button" onclick="',
  434.       mgr.tree.id, '.deleteAttribute()"></div>');
  435.  
  436.     // The function list panel.
  437.     arr.push('<div id="list-panel" class="list-panel">',
  438.         '<div id="list-sub-panel" class="list-sub-panel"></div></div>');
  439.  
  440.     // Complete the extras html content and inject it into the UI.
  441.     var html = arr.join('');
  442.     su.setContent('extras', html);
  443.   }
  444.  
  445.   // Cache references to the edit panel and scroll panel for better
  446.   // performance in routines below.
  447.   mgr.editPanel = $('edit-panel');
  448.   mgr.scrollPanel = $('scroll-panel');
  449.  
  450.   // Reset the active tab to be what it was when the user closed the panel.
  451.   var activeTab;
  452.   if (su.notEmpty(activeTab = su.retrieveFromCookie('activeTab'))) {
  453.     mgr.setTab(activeTab);
  454.   }
  455.  
  456.   // Draw the main attribute tree content next. This generates the actual
  457.   // tree/spreadsheet UI elements.
  458.   mgr.tree.render();
  459.  
  460.   // Update the status bar, which is defined in the original HTML content.
  461.   mgr.setStatusBar(mgr.INTRO_STATUS);
  462.  
  463.   // Disable text selection across most content areas.
  464.   var disableSelect = function(evt) { return false; };
  465.   $('highlight-panel').onselectstart = disableSelect;
  466.   $('header').onselectstart = disableSelect;
  467.   $('tab-panel').onselectstart = disableSelect;
  468.   $('list-panel').onselectstart = disableSelect;
  469.   $('footer').onselectstart = disableSelect;
  470.   $('message-panel').onselectstart = disableSelect;
  471.   $('functions-panel').onselectstart = disableSelect;
  472.   $('edit-field-reference-tab').onselectstart = disableSelect;
  473.  
  474.   mgr.scrollPanel.onselectstart = function(evt) {
  475.     var ev = evt || window.event;
  476.     var target = ev.target || ev.srcElement;
  477.  
  478.     // IE does not consistently return the nodeType one would expect, so the
  479.     // TEXTAREA tag check is a workaround for IE.
  480.     if (target.tagName == 'TEXTAREA') {
  481.       return true;
  482.     } else if (target.nodeType != Node.TEXT_NODE) {
  483.       return false;
  484.     }
  485.   };
  486.  
  487.   mgr.scrollPanel.onscroll = function(evt) {
  488.  
  489.     window.clearTimeout(mgr.finalScrollAdjustTimeout);
  490.  
  491.     mgr.finalScrollAdjustTimeout = window.setTimeout(function() {
  492.       mgr.floatEditorIfNecessary();
  493.       if (mgr.isEditing()) {
  494.         $('edit-field').focus();
  495.       }
  496.     }, 100);
  497.  
  498.     mgr.floatEditorIfNecessary();
  499.   };
  500.  
  501.   // If the user is selecting from the big list of attributes and they click
  502.   // outside onto the scroll panel hide the list panel the same way that
  503.   // standard select controls hide their options.
  504.   $('content').onmousedown = function(evt) {
  505.     if (su.isVisible('list-panel') &&
  506.       $('edit-field').value == su.translateString(mgr.ENTER_NAME_STRING)) {
  507.       mgr.tree.hideEditPanels();
  508.     }
  509.   };
  510.  
  511.   // Build the interface for the functions tab/select list.
  512.   arr = [];
  513.   arr.push('<table class="function-summary-table" cellspacing="0">', '<tr><td>',
  514.     '<select id="function-list" onchange="mgr.showFunctionSummary(this)">',
  515.     '<option class="function-list-item">',
  516.     su.translateString('Select a spreadsheet function...'),
  517.     '</option>');
  518.  
  519.   // Generate an option for each available function as defined in the
  520.   // components.js function list.
  521.   for (var functionSetName in comp.functionList) {
  522.  
  523.     arr.push('<option class="function-list-head">',
  524.         su.translateString(functionSetName));
  525.  
  526.     var functionArray = comp.functionList[functionSetName];
  527.     for (var i = 0; i < functionArray.length; i++) {
  528.       var functionData = functionArray[i];
  529.       arr.push('<option value="', su.translateString(functionData.summary),
  530.         '" class="function-list-item"> ',
  531.         su.translateString(functionData.name), '</option>');
  532.     }
  533.   }
  534.  
  535.   // Close the select list and its enclosing table data cell.
  536.   arr.push('</select></td>');
  537.  
  538.   // Next cell is the insert button which will inject the currently selected
  539.   // function into the edit cell at the cursor location. Setting width to 1%
  540.   // here keeps the input button pushed to the far right.
  541.   arr.push('<td width="1%"><input type="button" disabled="true"',
  542.     ' id="insert-button" onclick="mgr.insertFunction()"',
  543.     ' class="submit-button" value="', su.translateString('insert'), '">',
  544.     '</td></tr>',
  545.     '</table>');
  546.  
  547.   // Below the select list/input button pair we keep another table whose
  548.   // content is the summary text and a "more" link.
  549.   arr.push('<table class="function-summary-table" cellspacing="0"><tr>',
  550.     '<td><div id="function-summary"></div></td>',
  551.     '<td id="function-insert-cell"><a href="skp:do_open_url@url=',
  552.     // NOTE that this embedded URL has no quoting.
  553.     mgr.FUNCTIONS_URL, '"><b>',
  554.     su.translateString('more'), '»</b></a> ',
  555.     '</td></tr></table>');
  556.  
  557.   $('functions-panel').innerHTML = arr.join('');
  558.  
  559.   // Hide any of the extras we don't want to be visible on first view.
  560.   su.hide('details-panel');
  561.  
  562.   // If the user interface is being completely (re)built then we'll have to
  563.   // reset any focus/highlighting that might have been in place. This is due
  564.   // to the asynchronous nature of calls to the Ruby side of the bridge.
  565.   window.setTimeout(function() {
  566.     mgr.refocus();
  567.   }, 0);
  568.  
  569.   mgr.updateLayout();
  570. };
  571.  
  572. // ---
  573. // Display Management
  574. // ---
  575.  
  576. /**
  577.  * Responds to notifications that the manager panel is being resized. As a
  578.  * result the detail controls are hidden and the highlight or edit cell (if
  579.  * visible) are resized to fit their underlying cell.
  580.  */
  581. mgr.handleResize = function() {
  582.  
  583.   mgr.updateLayout();
  584.  
  585.   if (mgr.tree) {
  586.     mgr.tree.hideDetailControls();
  587.   }
  588.  
  589.   if (mgr.isDetailing()) {
  590.     mgr.updateDetailPanelLayout();
  591.   }
  592.  
  593.   if (mgr.isHighlighting()) {
  594.     mgr.tree.highlight();
  595.   }
  596.  
  597.   if (mgr.isEditing()) {
  598.  
  599.     window.clearTimeout(mgr.finalResizeAdjustTimeout);
  600.  
  601.     mgr.finalResizeAdjustTimeout = window.setTimeout(function() {
  602.     mgr.updateEditorLayout();
  603.     mgr.floatEditorIfNecessary();
  604.       if (mgr.isEditing()) {
  605.         $('edit-field').focus();
  606.       }
  607.     }, 100);
  608.  
  609.     mgr.updateEditorLayout();
  610.     mgr.floatEditorIfNecessary();
  611.     if (mgr.isFloating()) {
  612.       mgr.updateFloatingEditorLayout();
  613.     }
  614.   }
  615. };
  616.  
  617. /**
  618.  * Toggles formula view mode on or off, rendering the tree appropriately.
  619.  */
  620. mgr.toggleFormulaView = function() {
  621.  
  622.   if (mgr.showFormulas == true) {
  623.     mgr.showFormulas = false;
  624.     $('settings-button').className = 'settings-button';
  625.   } else {
  626.     mgr.showFormulas = true;
  627.     $('settings-button').className = 'settings-button-on';
  628.   }
  629.  
  630.   // Since this will redraw our panel, we need to store the refocus rule.
  631.   if (mgr.getFocusedElement()) {
  632.     mgr.refocusIndex = mgr.FOCUS_CURRENT;
  633.     mgr.refocusTarget = mgr.getFocusedElement().getAttribute('id');
  634.   }
  635.   mgr.tree.render();
  636.  
  637. };
  638.  
  639. /**
  640.  * Forces a redraw of the user interface.
  641.  */
  642. mgr.redraw = function() {
  643.   comp.pullAttributes({
  644.     'selection_ids': '',
  645.     'deep': true,
  646.     'oncomplete': 'mgr.handlePullAttributesComplete'
  647.   });
  648. };
  649.  
  650. /**
  651.  * Updates the layout of the detail panel, adjusting specific style
  652.  * properties to help ensure proper display.
  653.  */
  654. mgr.updateDetailPanelLayout = function() {
  655.   // Update the details panel formLabel field to be a fixed size that fits
  656.   // within its parent cell, regardless of the length of the content inside.
  657.   var formLabel = $('formlabel-textbox')
  658.   if (su.isValid(formLabel)) {
  659.     // Note that we first set the size to a fixed value, otherwise the
  660.     // subsequent call to su.elementWidth(formLabel.parentElement) returns
  661.     // zero on IE.
  662.     formLabel.style.width = 100;
  663.     formLabel.style.width = su.elementWidth(formLabel.parentElement);
  664.   }
  665. };
  666.  
  667. /**
  668.  * Updates the layout of the user interface, adjusting specific style
  669.  * properties to help ensure proper display.
  670.  */
  671. mgr.updateListPanelLayout = function() {
  672.   var listSubpanel;
  673.   var pageWidth;
  674.   var width;
  675.  
  676.   if (su.isValid(listSubpanel = $('list-sub-panel'))) {
  677.     var listPanel = $('list-panel');
  678.     var editField = $('edit-field');
  679.  
  680.     var popupHeight = su.elementHeight(listPanel);
  681.     var fieldHeight = su.elementHeight(editField);
  682.  
  683.     var panel = mgr.editPanel;
  684.     var scrollPanel = mgr.scrollPanel;
  685.  
  686.     var offset = su.elementY(scrollPanel);
  687.  
  688.     var scrollHeight = scrollPanel.scrollHeight;
  689.     var offsetHeight = su.elementHeight(scrollPanel);
  690.  
  691.     var scrollTop = scrollPanel.scrollTop;
  692.     var scrollBottom = scrollHeight - (scrollTop + offsetHeight);
  693.  
  694.     var panelTop = su.elementY(panel) - offset;
  695.     var panelHeight = su.elementHeight(panel);
  696.  
  697.     var scrollAbove = scrollTop + panelTop;
  698.     var scrollBelow = scrollBottom + (offsetHeight - panelTop -
  699.         panelHeight) - 12;
  700.  
  701.     var viewAbove = panelTop - 24;
  702.     var viewBelow = offsetHeight - (viewAbove + panelHeight) - (12 + 24);
  703.  
  704.     if (viewBelow > popupHeight) {
  705.       // First choice is to align with top and have it feel like a
  706.       // drop-down menu with everything in view. That requires that we have
  707.       // room in the "viewBelow" size for the entire panel.
  708.       listPanel.style.top = panelTop - fieldHeight + 12 - 4 + scrollTop;
  709.     } else if (viewAbove > popupHeight) {
  710.       // Second choice is if we have room above we can align with the bottom
  711.       // and place to align with the bottom of the field.
  712.       listPanel.style.top = panelTop - popupHeight + scrollTop + 12;
  713.     } else {
  714.       // Last option is that we try to position in the available space in
  715.       // the window, but since we already know it won't fit cleanly above or
  716.       // below we can just set the top to be just below the visible top.
  717.       listPanel.style.top = scrollTop + 12;
  718.     }
  719.  
  720.     // Avoid throwing off computations as we move between levels by pushing
  721.     // the panel to the left so it doesn't trigger scrollbars etc too early.
  722.     listPanel.style.left = 0;
  723.  
  724.     // If we're adjusting this panel's size we're showing an editor cell
  725.     // over the attribute name cell. That implies that the current target is
  726.     // the editor name cell. We want to be the size of that cell's value
  727.     // counterpart so we fit into the table regardless of scrollbars, window
  728.     // offsets, etc.
  729.  
  730.     var target = mgr.getFocusedElement();
  731.  
  732.     while ((target = target.nextSibling) &&
  733.         (target.nodeType != Node.ELEMENT_NODE)) {
  734.     }
  735.  
  736.     if (su.notValid(target)) {
  737.       // Fallback is to work from page width.
  738.       if (document.body.clientWidth) {
  739.         pageWidth = document.body.clientWidth || 0;
  740.       } else {
  741.         pageWidth = window.innerWidth || 0;
  742.       }
  743.  
  744.       width = Math.max(pageWidth - su.elementX(editField) -
  745.           su.elementWidth(editField) - (su.IS_MAC ? 30 : 15), 0);
  746.     } else {
  747.       width = su.elementWidth(target);
  748.     }
  749.  
  750.     listPanel.style.width = width;
  751.  
  752.     // Our left edge lines up with the editor, wherever it may be.
  753.     listPanel.style.left = su.elementX(editField) +
  754.         su.elementWidth(editField) + 1;
  755.   }
  756. };
  757.  
  758. // ---
  759. // Editor Management
  760. // ---
  761.  
  762. /**
  763.  * Returns true if the manager is currently displaying the details panel and
  764.  * hence is actively detailing a particular attribute.
  765.  * @return {boolean} True if the manager details panel is currently open.
  766.  */
  767. mgr.isDetailing = function() {
  768.   return su.isVisible('details-panel');
  769. };
  770.  
  771. /**
  772.  * Returns true if the manager is currently displaying the editor cell and
  773.  * hence is actively editing a particular attribute label or value.
  774.  * @return {boolean} True if the manager editing cell is currently open.
  775.  */
  776. mgr.isEditing = function() {
  777.   // As long as we're below mgr.HIDDDEN_EDITOR_TOP this allows scrolling
  778.   // quickly without causing editing state to be lost.
  779.   return su.isVisible('edit-panel') && (su.elementY('edit-panel') >
  780.       (mgr.HIDDEN_EDITOR_TOP + 1000));
  781. };
  782.  
  783. /**
  784.  * Combined setter/getter for the isFloating state, which is true when the
  785.  * edit panel has "torn off" and is floating over the attribute tree surface
  786.  * to keep the edit field in view during scrolling operations.
  787.  * @param {boolean} opt_flag True to set floating state to true.
  788.  * @return {boolean} True if the manager editing cell is floating.
  789.  */
  790. mgr.isFloating = function(opt_flag) {
  791.   if (su.isValid(opt_flag)) {
  792.     mgr._floating = opt_flag;
  793.   }
  794.  
  795.   return mgr._floating;
  796. };
  797.  
  798. /**
  799.  * Returns true if the manager is currently displaying a highlight rectangle.
  800.  * @return {boolean} True if the manager highlight rectangle is visible.
  801.  */
  802. mgr.isHighlighting = function() {
  803.   return su.isVisible('highlight-line-top');
  804. };
  805.  
  806. // ---
  807. // Event Handling
  808. // ---
  809.  
  810. /**
  811.  * Responds to requests to cancel any pending edits to an entity.
  812.  * @param {Object} entity The entity object to cancel edits for.
  813.  */
  814. mgr.doCancel = function(entity) {
  815.   var obj = su.notValid(entity) ? mgr.rootEntity : entity;
  816.   mgr.callRuby('do_close', {'id': obj.id});
  817. };
  818.  
  819. /**
  820.  * Responds to clicks in the background of the manager panel, ensuring that
  821.  * any pending edits commit when the user clicks away.
  822.  * @param {Event} evt The native click event.
  823.  */
  824. mgr.doPending = function(evt) {
  825.  
  826.   if (!mgr.isEditing()) {
  827.     return;
  828.   }
  829.  
  830.   // Check for DIV so we allow TD and editor cells to be clicked without
  831.   // triggering handleEdit (which would be bad).
  832.   var ev = evt || window.event;
  833.   var target = ev.target || ev.srcElement;
  834.   if (!target || target.tagName.toUpperCase() != 'DIV') {
  835.     return;
  836.   }
  837.   if (target.getAttribute('class') == 'add-attribute-link') {
  838.     return;
  839.   }
  840.  
  841.   mgr.tree.handleEdit();
  842. };
  843.  
  844. /**
  845.  * Respond to requests to delete an attribute from an entity. The user
  846.  * interface is redrawn after this request completes.
  847.  * @param {Object} entity The entity object to modify.
  848.  * @param {String} attribute The name of the attribute to remove.
  849.  */
  850. mgr.doDeleteAttribute = function(entity, attribute) {
  851.  
  852.   mgr.hideHighlight();
  853.  
  854.   if (mgr.tree.attNameToDetail == attribute) {
  855.     mgr.tree.idToDetail = null;
  856.     mgr.tree.attNameToDetail = null;
  857.     mgr.tree.lastAttributeSelected = null;
  858.     mgr.lastElementSelected = null;
  859.   }
  860.  
  861.   mgr.callRuby('do_delete_attribute', {
  862.     'id': entity.id,
  863.     'dictionary': comp.DICTIONARY,
  864.     'oncomplete': 'mgr.redraw',
  865.     'name': attribute
  866.   });
  867. };
  868.  
  869. /**
  870.  * Responds to requests to refresh the UI of the manager panel. Commonly
  871.  * invoked from the Refresh button in manager.html.
  872.  */
  873. mgr.doRefresh = function() {
  874.  
  875.   // Before we refresh, store the current scroll position in a cookie
  876.   // so we can keep the same scroll position.
  877.   su.storeToCookie('panelScrollTop', $('content').scrollTop);
  878.  
  879.   // Press the refresh button.
  880.   $('refresh-button').className = 'refresh-button-on';
  881.  
  882.   // Hide the details panel.
  883.   if (su.isValid(mgr.tree)) {
  884.     mgr.tree.hideDetailPanel();
  885.   }
  886.  
  887.   // Since this will redraw our panel, we need to store the refocus rule.
  888.   if (su.isValid(mgr.getFocusedElement())) {
  889.     mgr.refocusIndex = mgr.FOCUS_CURRENT;
  890.     mgr.refocusTarget = mgr.getFocusedElement().getAttribute('id');
  891.   }
  892.  
  893.   if (mgr.isEditing()) {
  894.     // If edits are in progress we'll let the handleEdit routine deal with
  895.     // them and then redraw as needed.
  896.     if (!mgr.tree.handleEdit($('edit-field'))) {
  897.       mgr.isCalling(true);
  898.       mgr.showCurtain();
  899.       mgr.redraw();
  900.     }
  901.   } else {
  902.     mgr.isCalling(true);
  903.     mgr.showCurtain();
  904.     mgr.redraw();
  905.   }
  906. };
  907.  
  908. // ---
  909. // Focus Management
  910. // ---
  911.  
  912. /**
  913.  * Returns the element which the editor is currently, or was most recently,
  914.  * asked to edit.
  915.  * @return {Element} The element whose content was last used for
  916.  *     editing.
  917.  */
  918. mgr.getEditorTarget = function() {
  919.   return mgr.editorTarget;
  920. };
  921.  
  922. /**
  923.  * Sets the element which the editor is currently, or was most recently,
  924.  * asked to edit.
  925.  * @param {Element} target The element whose content was last used for
  926.  *     editing.
  927.  */
  928. mgr.setEditorTarget = function(target) {
  929.   mgr.editorTarget = target;
  930. };
  931.  
  932. /**
  933.  * Returns the currently focused element.
  934.  * @return {Element?} The currently focused element.
  935.  */
  936. mgr.getFocusedElement = function() {
  937.   return mgr.lastElementSelected;
  938. };
  939.  
  940. /**
  941.  * Sets the currently focused element.
  942.  * @param {Element?} element The currently focused element.
  943.  */
  944. mgr.setFocusedElement = function(element) {
  945.   mgr.lastElementSelected = element;
  946. };
  947.  
  948. /**
  949.  * Moves the focus and/or highlight to the next focusable element in document
  950.  * order. If editing is currently active it will be moved to the new element,
  951.  * otherwise only the highlight effect will be moved.
  952.  * @param {string|Element} opt_elementOrID The element or element ID to find.
  953.  *     Default is the current actively focused element.
  954.  * @param {boolean} opt_forceEdit True to force edit field display.
  955.  * @return {Element?} The newly focused element.
  956.  */
  957. mgr.focusNext = function(opt_elementOrID, opt_forceEdit) {
  958.   return mgr.moveFocus(opt_elementOrID, mgr.FOCUS_NEXT, opt_forceEdit);
  959. };
  960.  
  961. /**
  962.  * Moves the focus to the previous focusable element in document order.
  963.  * If editing is currently active it will be moved to the new element,
  964.  * otherwise only the highlight effect will be moved.
  965.  * @param {string|Element} opt_elementOrID The element or element ID to find.
  966.  *     Default is the current actively focused element.
  967.  * @param {boolean} opt_forceEdit True to force edit field display.
  968.  * @return {Element?} The newly focused element.
  969.  */
  970. mgr.focusPrevious = function(opt_elementOrID, opt_forceEdit) {
  971.   return mgr.moveFocus(opt_elementOrID, mgr.FOCUS_PREVIOUS, opt_forceEdit);
  972. };
  973.  
  974. /**
  975.  * Moves the focus from an element in either a forward or backward direction
  976.  * in terms of document order. If editing is currently active it will be moved
  977.  * to the new element, otherwise only the highlight effect will be moved.
  978.  * @param {string|Element} opt_elementOrID The element or element ID to find.
  979.  *     Default is the current actively focused element.
  980.  * @param {number} opt_direction Either mgr.FOCUS_NEXT or mgr.FOCUS_PREVIOUS.
  981.  * @param {boolean} opt_forceEdit True to force edit field display.
  982.  * @return {Element?} The newly focused element.
  983.  */
  984. mgr.moveFocus = function(opt_elementOrID, opt_direction, opt_forceEdit) {
  985.   var el;
  986.  
  987.   if (su.isValid(opt_elementOrID)) {
  988.     el = $(opt_elementOrID);
  989.   } else {
  990.     el = mgr.getFocusedElement();
  991.   }
  992.  
  993.   var direction = su.isValid(opt_direction) ? opt_direction : mgr.FOCUS_NEXT;
  994.   if (su.isValid(el)) {
  995.  
  996.     var name = el.getAttribute('name');
  997.     if (su.notEmpty(name)) {
  998.       var match = name.match(/(.*)_(.*)/);
  999.       if (su.isValid(match)) {
  1000.  
  1001.         var nextID = 'field_' + (parseInt(match[2], null) + direction);
  1002.         // Note that we pass the parent.parent here to create a context node
  1003.         // for the search that's high enough to find the peer we need. If we
  1004.         // can't go up that far we stop at the parent.
  1005.         var ancestor = el.parentNode;
  1006.         if (su.isValid(ancestor)) {
  1007.           ancestor = ancestor.parentNode;
  1008.           if (su.notValid(ancestor)) {
  1009.             ancestor = el.parentNode;
  1010.           }
  1011.           var nextElement = $(nextID, ancestor);
  1012.           if (su.isValid(nextElement)) {
  1013.             var focusEl = mgr.setFocus(nextElement, opt_forceEdit);
  1014.             // Refresh value of the field we left to force redraw after the
  1015.             // edit panel has moved on and 'uncovered' the old field.
  1016.             el.innerHTML = el.innerHTML;
  1017.           }
  1018.         }
  1019.       }
  1020.     }
  1021.   }
  1022.  
  1023.   return focusEl;
  1024. };
  1025.  
  1026. /**
  1027.  * Reestablishes focus, typically after the AttributeTree instance has been
  1028.  * replaced with a new instance in response to a call to Ruby.
  1029.  */
  1030. mgr.refocus = function() {
  1031.  
  1032.   // Reset our last selection state.
  1033.   mgr.resetLastSelection();
  1034.  
  1035.   if (su.isEmpty(mgr.refocusIndex) || su.isEmpty(mgr.refocusTarget)) {
  1036.     return;
  1037.   }
  1038.  
  1039.   switch (mgr.refocusIndex) {
  1040.     case mgr.FOCUS_NEXT:
  1041.       mgr.focusNext(mgr.refocusTarget, mgr.refocusEditor);
  1042.       break;
  1043.     case mgr.FOCUS_PREVIOUS:
  1044.       mgr.focusPrevious(mgr.refocusTarget, mgr.refocusEditor);
  1045.       break;
  1046.     default:
  1047.       mgr.setFocus(mgr.refocusTarget, mgr.refocusEditor);
  1048.       break;
  1049.   }
  1050.  
  1051.   // Be sure to clear so we don't get out of sync with future operations.
  1052.   mgr.refocusTarget = null;
  1053.   mgr.refocusIndex = null;
  1054.   mgr.refocusEditor = null;
  1055. };
  1056.  
  1057. /**
  1058.  * Sets the focus/highlight to the element or element ID provided.
  1059.  * @param {string|Element} elementOrID The element or element ID to focus.
  1060.  *     Default is the current actively focused element.
  1061.  * @param {boolean} opt_forceEdit True to force edit field display.
  1062.  * @return {Element?} The newly focused element.
  1063.  */
  1064. mgr.setFocus = function(elementOrID, opt_forceEdit) {
  1065.   var el = $(elementOrID);
  1066.   if (su.notValid(el)) {
  1067.     // Element not found.
  1068.     return;
  1069.   }
  1070.  
  1071.   // Compute entity and attribute names from the element's ID, then
  1072.   // get the entity so we can properly query for the old value.
  1073.   var id = el.getAttribute('id');
  1074.   if (su.isEmpty(id)) {
  1075.     // Element has no ID data to compute entity/attribute from.
  1076.     return;
  1077.   }
  1078.  
  1079.   var parts = mgr.parseIdIntoParts(id);
  1080.   if (parts.length < 3) {
  1081.     // Element ID isn't in a prefix_entity_attribute format.
  1082.     return;
  1083.   }
  1084.  
  1085.   var entity = su.findEntity(parts[1], mgr.rootEntity);
  1086.   if (su.notValid(entity)) {
  1087.     // Specified entity isn't in our current data set.
  1088.     return;
  1089.   }
  1090.  
  1091.   // Update the properties which drive navigation and value
  1092.   // operations.
  1093.   mgr.tree.lastEntitySelected = entity;
  1094.   mgr.tree.lastAttributeSelected = comp.getAttribute(entity, parts[2]);
  1095.   mgr.setFocusedElement(el);
  1096.  
  1097.   // If we're in edit mode, then stay in edit mode, unless the next attribute
  1098.   // to edit is scaletool, in which case we only highlight. Scaletool can't
  1099.   // be edited directly with a text box.
  1100.   if ((opt_forceEdit || mgr.isEditing()) && parts[2] != 'scaletool') {
  1101.     mgr.tree.editAttributeValue(mgr.getFocusedElement());
  1102.   } else {
  1103.     mgr.tree.hideEditPanels();
  1104.   }
  1105.   mgr.tree.highlight(el, parts[1], parts[2]);
  1106.  
  1107.   return el;
  1108. };
  1109.  
  1110. // ---
  1111. // Selection Management
  1112. // ---
  1113.  
  1114. /**
  1115.  * Instructs the dialog to "select" an entity based on its ID. This function
  1116.  * both alters the local UI and pushes the selection down to SketchUp.
  1117.  * @param {string} id The id number of the entity to select. If null, then
  1118.  *     SketchUp will be instructed to clear out its selection.
  1119.  */
  1120. mgr.pushSelection = function(id) {
  1121.   var lastID;
  1122.   var el;
  1123.  
  1124.   // First clear any existing selection style.
  1125.   if (su.isValid(lastID = mgr.lastSelectionID)) {
  1126.     el = $('attribute-head-' + lastID);
  1127.     if (su.isValid(el)) {
  1128.       el.style.backgroundColor = '';
  1129.     }
  1130.   }
  1131.  
  1132.   // Then apply.
  1133.   if (su.isValid(id)) {
  1134.     el = $('attribute-head-' + id);
  1135.     mgr.lastSelectionID = id;
  1136.   }
  1137.  
  1138.   // Push the selection down to SketchUp.
  1139.   comp.pushSelection(id);
  1140. };
  1141.  
  1142. /**
  1143.  * Instructs the dialog to toggle the selection of an entity based on its ID.
  1144.  * @param {string} id The id number of the entity to toggle.
  1145.  */
  1146. mgr.toggleSelection = function(id) {
  1147.   if (id == mgr.lastSelectionID) {
  1148.     mgr.clearSelection();
  1149.   } else {
  1150.     mgr.pushSelection(id);
  1151.   }
  1152. };
  1153.  
  1154. /**
  1155.  * Clears any existing selection. Note that this function is called directly
  1156.  * by SketchUp, so it should not be renamed.
  1157.  */
  1158. mgr.clearSelection = function() {
  1159.   if (su.isValid(mgr.lastSelectionID)) {
  1160.     mgr.pushSelection(null);
  1161.   }
  1162.   mgr.lastSelectionID = null;
  1163. };
  1164.  
  1165. /**
  1166.  * Called on refresh of the dialog. This function restores any previous
  1167.  * selection.
  1168.  */
  1169. mgr.resetLastSelection = function() {
  1170.   mgr.pushSelection(mgr.lastSelectionID);
  1171. };
  1172.  
  1173. /**
  1174.  * Hides the highlight elements.
  1175.  */
  1176. mgr.hideHighlight = function() {
  1177.  
  1178.   mgr.setStatusBar(mgr.INTRO_STATUS);
  1179.  
  1180.   su.hide('highlight-line-top');
  1181.   su.hide('highlight-line-right');
  1182.   su.hide('highlight-line-bottom');
  1183.   su.hide('highlight-line-left');
  1184.  
  1185.   // Clear any previous label selection.
  1186.   if (su.isValid(mgr.tree.lastLabelCell)) {
  1187.     var cell = mgr.tree.lastLabelCell;
  1188.     var lastClass = cell.className;
  1189.     cell.className = lastClass.replace(/label-selected/, 'label');
  1190.   }
  1191.  
  1192.   // Hide details controls.
  1193.   if (su.isValid(mgr.tree)) {
  1194.     mgr.tree.hideDetailControls();
  1195.   }
  1196. };
  1197.  
  1198. // ---
  1199. // Value Cell
  1200. // ---
  1201.  
  1202. /**
  1203.  * Resets the value cell of the last edited attribute. This is called when
  1204.  * the user hits the ESC key when editing, or tabs away when they haven't
  1205.  * made any changes.
  1206.  * @param {string} opt_entityName The name of the entity providing values.
  1207.  *     Default is the last entity selected.
  1208.  * @return {boolean} True if the value was successfully reset.
  1209.  */
  1210. mgr.resetValueCell = function(opt_entityName) {
  1211.  
  1212.   // Assume that we have no error prefix on our attribute value.
  1213.   var errorPrefix = '';
  1214.  
  1215.   var unitGroup = '';
  1216.   var attribute = mgr.tree.lastAttributeSelected;
  1217.  
  1218.   if (su.isValid(attribute)) {
  1219.     var entity = mgr.tree.lastEntitySelected;
  1220.     var entityID = mgr.tree.lastEntitySelected.id;
  1221.     var name = mgr.tree.lastAttributeSelected.label;
  1222.  
  1223.     if (su.isValid(name)) {
  1224.       name = name.toLowerCase();
  1225.  
  1226.       var cell = $('value_' + entityID + '_' + name);
  1227.       if (su.isValid(cell)) {
  1228.  
  1229.         if (su.isValid(comp.RESERVED[name])) {
  1230.           unitGroup = comp.RESERVED[name].unitGroup;
  1231.         }
  1232.  
  1233.         var value = mgr.formatDisplayValue(entity, name);
  1234.  
  1235.         var error = attribute.error;
  1236.         if (su.isValid(error)) {
  1237.           if (error.indexOf('subformula-error') > -1) {
  1238.             value = '=' + error;
  1239.             errorPrefix = mgr.ERROR_PREFIX;
  1240.           }
  1241.         }
  1242.  
  1243.         // Reset any size that was set in updateEditorLayout.
  1244.         cell.style.height = 'auto';
  1245.  
  1246.         if (mgr.showFormulas == true && su.isDefined(attribute.formula)) {
  1247.           cell.innerHTML = errorPrefix + '=' +
  1248.               mgr.insertSoftBreaks(attribute.formula);
  1249.         } else {
  1250.           cell.innerHTML = errorPrefix + value + ' ';
  1251.         }
  1252.       }
  1253.     }
  1254.   }
  1255.   return true;
  1256. };
  1257.  
  1258. /**
  1259.  * Clears the HTML value cell for a given entity and attribute. This makes
  1260.  * refreshing the display a little cleaner looking when called before waiting
  1261.  * for a jsCallback to update the whole page.
  1262.  * @param {Object} entity The entity object we're operating on.
  1263.  * @param {string} attribute The attribute name defining which value cell.
  1264.  * @param {string} value The new value for the entity/attribute cell.
  1265.  */
  1266. mgr.setValueCell = function(entity, attribute, value) {
  1267.   su.setContent('value_' + entity.id + '_' + attribute, value);
  1268. };
  1269.  
  1270. // ---
  1271. // Utilities
  1272. // ---
  1273.  
  1274. /**
  1275.  * Resets the scale tool graphic.
  1276.  * @return {number} The computed scale tool size.
  1277.  */
  1278. mgr.calculateScaleTool = function() {
  1279.   var result = 0;
  1280.   var mask = 1;
  1281.  
  1282.   for (var i = 1; i <= 7; i++) {
  1283.     if (!$('scaletool_' + i).checked) {
  1284.       result += mask;
  1285.     }
  1286.     mask *= 2;
  1287.   }
  1288.  
  1289.   $('scale-tool-cell').innerHTML = mgr.dumpScaleToolGraphic(result);
  1290.   return result;
  1291. };
  1292.  
  1293. /**
  1294.  * Outputs HTML to display the scale tool graphic in the details panel.
  1295.  * @param {number} value The scale tool cell size to compute from.
  1296.  * @return {string} HTML containing a generated scale tool graphic.
  1297.  */
  1298. mgr.dumpScaleToolGraphic = function(value) {
  1299.   var arr = [];
  1300.   arr.push('<br/><div class="scaletool-graphic">');
  1301.  
  1302.   for (var i = 1; i <= 7; i++) {
  1303.     var html = '<div class="scaletool-graphic" style="background-position: -';
  1304.     html += i * 100;
  1305.     html += 'px 0px">';
  1306.     arr.push(mgr.ifBitIsZero(value, i, html));
  1307.   }
  1308.  
  1309.   for (i = 1; i <= 7; i++) {
  1310.     arr.push(mgr.ifBitIsZero(value, i, '</div>'));
  1311.   }
  1312.  
  1313.   arr.push('</div>');
  1314.   return arr.join('');
  1315. };
  1316.  
  1317. /**
  1318.  * Returns a properly formatted string for a given attribute and its
  1319.  * unitGroup.
  1320.  * @param {Object} entity The entity object providing the data.
  1321.  * @param {string} attribute The attribute name to query for.
  1322.  * @return {string} HTML containing a formatted display value.
  1323.  */
  1324. mgr.formatDisplayValue = function(entity, attribute) {
  1325.   var displayValue;
  1326.  
  1327.   // The scaletool doesn't make sense to format via the
  1328.   // comp.getAttributeFormattedValue method so work around that here.
  1329.   if (attribute == 'scaletool') {
  1330.     var attr = comp.getAttribute(entity, attribute);
  1331.     if (attr.value == '' || attr.value == 0) {
  1332.       displayValue = '<span class="live-value-result">' +
  1333.           su.translateString('All scale handles visible.') + '</span>';
  1334.     } else if (attr.value == mgr.ALL_SCALE_HANDLES_HIDDEN) {
  1335.       displayValue = '<span class="live-value-result">' +
  1336.           su.translateString('All scale handles hidden.') + '</span>';
  1337.     } else {
  1338.       displayValue = '<span class="live-value-result">' +
  1339.           su.translateString('Some scale handles hidden.') + '</span>';
  1340.     }
  1341.     return displayValue;
  1342.   }
  1343.  
  1344.   displayValue = comp.getAttributeFormattedValue(entity, attribute);
  1345.   displayValue = displayValue.replace(/\</g, '<');
  1346.   displayValue = displayValue.replace(/\>/g, '>');
  1347.   displayValue = displayValue.replace(/\&\#92;/g, '\\');
  1348.   displayValue = displayValue.replace(/\//g,
  1349.       '/<span class="zero-width"> </span>');
  1350.  
  1351.   displayValue = mgr.insertSoftBreaks(displayValue);
  1352.   return displayValue;
  1353. };
  1354.  
  1355. /**
  1356.  * Checks an integer to see if a given bit position is 0. If so, returns a
  1357.  * given string.
  1358.  * @param {string} value The integer's string representation.
  1359.  * @param {number} bitPos The bit position to check.
  1360.  * @param {string} result The string to return on success.
  1361.  * @return {Object} The result value when the test is successful, or ''.
  1362.  */
  1363. mgr.ifBitIsZero = function(value, bitPos, result) {
  1364.   var mask = 1;
  1365.   var maskBits = 1;
  1366.   var intValue = parseInt(value, null);
  1367.  
  1368.   for (var i = 1; i < bitPos; i++) {
  1369.     mask = mask + maskBits;
  1370.     maskBits = maskBits << 1;
  1371.   }
  1372.  
  1373.   if ((intValue & mask) == mask) {
  1374.     return '';
  1375.   } else {
  1376.     return result;
  1377.   }
  1378. };
  1379.  
  1380. /**
  1381.  * Inserts the currently selected function into the edit field.
  1382.  * @return {boolean} Typically returns false to avoid event handler issues.
  1383.  */
  1384. mgr.insertFunction = function() {
  1385.  
  1386.   // Do not allow inserting functions into names.
  1387.   if (mgr.tree.isEditingName == true) {
  1388.     return false;
  1389.   }
  1390.  
  1391.   var field = $('edit-field');
  1392.  
  1393.   if (mgr.isEditing() && !mgr.isDetailing()) {
  1394.     field.focus();
  1395.     mgr.storeSelectionTextRange();
  1396.  
  1397.     if (!su.IS_MAC) {
  1398.       if (mgr.selectionTextRange.parentElement() != field) {
  1399.         return;
  1400.       }
  1401.     }
  1402.  
  1403.     var selectObj = $('function-list');
  1404.     var optionObj = selectObj.options[selectObj.selectedIndex];
  1405.     var insertText = optionObj.text;
  1406.     insertText = insertText.substr(1);
  1407.     insertText = insertText.replace(/\s\(/gi, '(');
  1408.  
  1409.     su.replaceSelection(field, insertText, mgr.selectionTextRange);
  1410.  
  1411.     mgr.updateEditorLayout();
  1412.     field.focus();
  1413.   } else if (mgr.isDetailing()) {
  1414.     field = document.activeElement;
  1415.     if (field != $('edit-field')) {
  1416.       su.replaceSelection(field, insertText, mgr.selectionTextRange);
  1417.       field.focus();
  1418.     }
  1419.   }
  1420. };
  1421.  
  1422. /**
  1423.  * Inserts characters into a string value to force the browser to break long
  1424.  * words for easier display.
  1425.  * @param {string} displayValue The string value to break.
  1426.  * @return {string} The newly formed string with spaces injected.
  1427.  */
  1428. mgr.insertSoftBreaks = function(displayValue) {
  1429.  
  1430.   // Force-convert our value to a string for display purposes. Note that the
  1431.   // '' value should come first so we're effectively messaging a string and
  1432.   // asking it to add a value (which will be converted into a string).
  1433.   var result = '' + displayValue;
  1434.  
  1435.   // Insert zero-width breaks after punctuation marks.
  1436.   result = result.replace(
  1437.       /([\:\!\@\#\$\%\^\;\*\(\(\+\{\[\}\]\|\,\.\?])(?=\w)/g,
  1438.       '$1<span class="zero-width"> </span>');
  1439.  
  1440.   // Insert the zero-width character before semicolons, so we don't cause
  1441.   // parsing problems with   or "
  1442.   result = result.replace(/([\&\<])/g, '<span class="zero-width"> </span>$1');
  1443.  
  1444.   // Break really long strings of work characters into 10-char chunks.
  1445.   result = result.replace(/(\w{10})(\w)/g,
  1446.       '$1<span class="zero-width"> </span>$2');
  1447.  
  1448.   return result;
  1449. };
  1450.  
  1451. /**
  1452.  * Prompts for a new entity name and saves it, redrawing the user interface
  1453.  * upon completion.
  1454.  * @param {string} entityID The ID of the entity being renamed.
  1455.  * @param {string} currentName The current entity name.
  1456.  */
  1457. mgr.renameEntity = function(entityID, currentName) {
  1458.   var entity = su.findEntity(entityID, mgr.rootEntity);
  1459.   var message = 'Please enter a new object name:'
  1460.  
  1461.   var response = prompt(su.translateString(message), currentName);
  1462.  
  1463.   // Exit if the user clicked the cancel button.
  1464.   if (response == null) {
  1465.     return;
  1466.   }
  1467.  
  1468.   // Clean off any leading or trailing white space.
  1469.   response = response.replace(/^\s+/gi, '');
  1470.   response = response.replace(/\s+$/gi, '');
  1471.  
  1472.   if (response.toLowerCase() == 'model') {
  1473.     alert(su.translateString('You are not allowed to use the name "model".' +
  1474.       ' Please try a different name.'));
  1475.     mgr.renameEntity(entityID, currentName);
  1476.   } else if (response.toLowerCase() == 'parent') {
  1477.     alert(su.translateString('You are not allowed to use the name "parent".' +
  1478.       ' Please try a different name.'));
  1479.     mgr.renameEntity(entityID, currentName);
  1480.   } else if (su.isEmpty(response)) {
  1481.     alert(su.translateString('You are not allowed to use a blank name.'));
  1482.     mgr.renameEntity(entityID, currentName);
  1483.   } else if (response.match(/^[\d\.]+$/)) {
  1484.     alert(su.translateString('You are not allowed to create names ' +
  1485.       'that are numbers.'));
  1486.     mgr.renameEntity(entityID, response);
  1487.   } else if (response.match(/[\+\-\*\/\(\)\<\>\=\^\"]/) !== null) {
  1488.     alert(su.translateString('You are not allowed to create names ' +
  1489.       'containing mathematical symbols.'));
  1490.     mgr.renameEntity(entityID, response);
  1491.   } else if (response.length > mgr.MAX_NAME_LENGTH) {
  1492.     alert(su.translateString('You are not allowed to use extremely long ' +
  1493.       'names. Please try a shorter name.'));
  1494.     mgr.renameEntity(entityID, response);
  1495.   } else if (response != currentName) {
  1496.     comp.setAttributeValue(entity, '_name', response);
  1497.     comp.pushAttribute(entity, '_name', 'mgr.redraw');
  1498.   }
  1499. };
  1500.  
  1501. /**
  1502.  * Sets the value(s) in the status bar for the manager panel.
  1503.  * @param {string} str The overall status string for the status bar.
  1504.  * @param {string} opt_iconName The name of an optional icon to display.
  1505.  */
  1506. mgr.setStatusBar = function(str, opt_iconName) {
  1507.   su.setContent('mgr-status', str);
  1508.  
  1509.   if (su.isValid(comp.RESERVED[opt_iconName])) {
  1510.     $('mgr-icon').className = 'mgr-icon-' + opt_iconName +
  1511.         ' mgr-icon-' + comp.RESERVED[opt_iconName].group.replace(/\s/gi, '');
  1512.   } else {
  1513.     $('mgr-icon').className = 'mgr-icon-' + su.ifEmpty(opt_iconName, '');
  1514.   }
  1515. };
  1516.  
  1517. /**
  1518.  * Promotes the tab named by tabName to be the active tab.
  1519.  * @param {string} tabName The name of the tab (div) to activate.
  1520.  */
  1521. mgr.setTab = function(tabName) {
  1522.  
  1523.   // Reset the tab classes.
  1524.   $('tab-basic').className = 'tab';
  1525.   $('tab-cap-basic').className = 'tab-cap';
  1526.   $('tab-functions').className = 'tab';
  1527.   $('tab-cap-functions').className = 'tab-cap';
  1528.  
  1529.   // Show the appropriate panel.
  1530.   su.hide('functions-panel');
  1531.  
  1532.   su.show(tabName + '-panel');
  1533.  
  1534.   if (su.isValid($('tab-' + tabName))) {
  1535.     $('tab-' + tabName).className = 'tab-selected';
  1536.     $('tab-cap-' + tabName).className = 'tab-cap-selected';
  1537.   }
  1538.  
  1539.   if (mgr.isEditing()) {
  1540.     $('edit-field').focus();
  1541.   }
  1542.  
  1543.   su.storeToCookie('activeTab', tabName);
  1544. };
  1545.  
  1546. /**
  1547.  * Displays a function summary string based on the function pulldown value.
  1548.  * @param {Element} selectObj The HTML 'select' element containing the
  1549.  *     function list.
  1550.  */
  1551. mgr.showFunctionSummary = function(selectObj) {
  1552.   var optionObj = selectObj.options[selectObj.selectedIndex];
  1553.  
  1554.   if (optionObj.className.indexOf('head') > -1 ||
  1555.       selectObj.selectedIndex == 0 || su.isEmpty(selectObj.value)) {
  1556.     $('function-summary').innerHTML = '';
  1557.     $('insert-button').disabled = true;
  1558.   } else {
  1559.     $('function-summary').innerHTML = selectObj.value;
  1560.     $('insert-button').disabled = false;
  1561.   }
  1562.  
  1563.   if (mgr.isEditing()) {
  1564.     $('edit-field').focus();
  1565.   }
  1566. };
  1567.  
  1568. /**
  1569.  * Stores the current text selection of the user so it can be replaced when a
  1570.  * partial selection was made.
  1571.  */
  1572. mgr.storeSelectionTextRange = function() {
  1573.   var selection;
  1574.   mgr.selectionTextRange = null;
  1575.   if (!su.IS_MAC) {
  1576.     if (su.isValid(selection = document.selection)) {
  1577.       mgr.selectionTextRange = selection.createRange();
  1578.     }
  1579.   }
  1580. };
  1581.  
  1582. /**
  1583.  * Updates the highlight divs to match the last highlighted cell.
  1584.  * @param {boolean} opt_show Should the update also display the highlight
  1585.  *     elements. By default the value is false.
  1586.  */
  1587. mgr.updateHighlightLayout = function(opt_show)  {
  1588.  
  1589.   if (su.notValid(mgr.tree) || su.notValid(mgr.tree.lastLabelCell)) {
  1590.     return;
  1591.   }
  1592.  
  1593.   // Given the lastLabelCell, we want to find the value cell, which will be the
  1594.   // second TD inside the TR which holds both the label and value.
  1595.   var ancestor = mgr.tree.lastLabelCell.parentNode;
  1596.   if (su.notValid(ancestor)) {
  1597.     return;
  1598.   }
  1599.   var cell = ancestor.getElementsByTagName('TD')[1];
  1600.   var offset = mgr.HIGHLIGHT_EDIT_OFFSET - mgr.scrollPanel.scrollTop;
  1601.  
  1602.   var elem = $('highlight-line-top');
  1603.   elem.style.top = su.elementY(cell) - offset;
  1604.   elem.style.left = su.elementX(cell);
  1605.   elem.style.width = su.elementWidth(cell);
  1606.   elem.style.height = 2;
  1607.   if (opt_show == true) {
  1608.     su.show(elem);
  1609.   }
  1610.  
  1611.   elem = $('highlight-line-bottom');
  1612.   elem.style.top = su.elementY(cell) + su.elementHeight(cell) - 2 - offset;
  1613.   elem.style.left = su.elementX(cell);
  1614.   elem.style.width = su.elementWidth(cell);
  1615.   elem.style.height = 2;
  1616.   if (opt_show == true) {
  1617.     su.show(elem);
  1618.   }
  1619.  
  1620.  
  1621.   elem = $('highlight-line-left');
  1622.   elem.style.top = su.elementY(cell) - offset;
  1623.   elem.style.left = su.elementX(cell);
  1624.   elem.style.width = 2;
  1625.   elem.style.height = su.elementHeight(cell);
  1626.   if (opt_show == true) {
  1627.     su.show(elem);
  1628.   }
  1629.  
  1630.   elem = $('highlight-line-right');
  1631.   elem.style.top = su.elementY(cell) - offset;
  1632.   elem.style.left = su.elementX(cell) + su.elementWidth(cell) - 2;
  1633.   elem.style.width = 2;
  1634.   elem.style.height = su.elementHeight(cell);
  1635.   if (opt_show == true) {
  1636.     su.show(elem);
  1637.   }
  1638. };
  1639.  
  1640. /**
  1641.  * Updates the layout of the panel. Note that this is only actively used by IE
  1642.  * as Safari's CSS engine can manage the interface automatically.
  1643.  */
  1644. mgr.updateLayout = function() {
  1645.   var elem;
  1646.  
  1647.   if (su.IS_MAC) {
  1648.     return;
  1649.   }
  1650.  
  1651.   elem = $('inspector-panel');
  1652.   if (su.isValid(elem)) {
  1653.     try {
  1654.       elem.style.height = elem.offsetParent.offsetHeight - 28 + 'px';
  1655.     } catch (e) {
  1656.       // Ignore when new value(s) aren't viable.
  1657.     }
  1658.   }
  1659.  
  1660.   elem = $('scroll-panel');
  1661.   if (su.isValid(elem)) {
  1662.     try {
  1663.       elem.style.height = elem.offsetParent.offsetHeight - 71 + 'px';
  1664.     } catch (e) {
  1665.       // Ignore when new value(s) aren't viable.
  1666.     }
  1667.   }
  1668.  
  1669.   elem = $('details-panel');
  1670.   if (su.isValid(elem)) {
  1671.     try {
  1672.       elem.style.width = elem.offsetParent.offsetWidth + 'px';
  1673.       elem.style.height = elem.offsetParent.offsetHeight + 'px';
  1674.     } catch (e) {
  1675.       // Ignore when new value(s) aren't viable.
  1676.     }
  1677.   }
  1678.  
  1679.   elem = $('details-sub-panel');
  1680.   if (su.isValid(elem)) {
  1681.     try {
  1682.       elem.style.width = elem.offsetParent.offsetWidth - 6 + 'px';
  1683.       elem.style.height = elem.offsetParent.offsetHeight - 80 + 'px';
  1684.     } catch (e) {
  1685.       // Ignore when new value(s) aren't viable.
  1686.     }
  1687.   }
  1688. };
  1689.  
  1690. /**
  1691.  * Updates the field editor to be the same size and location as the field it
  1692.  * is editing. The editor itself is adjusted first to adapt to new input (or
  1693.  * initial input if the cell is being shown for the first time). The
  1694.  * underlying field is then adjusted to match the height of the editor cell.
  1695.  */
  1696. mgr.updateEditorLayout = function() {
  1697.  
  1698.   var target = mgr.getFocusedElement();
  1699.  
  1700.   // If the target is invalid then exit. Note that on IE there are cases
  1701.   // where updateEditorLayout can be called when we are not in edit mode. So
  1702.   // if we reach this point and we are not editing we can exit.
  1703.   if (!su.isValid(target) || !mgr.isEditing()) {
  1704.     return;
  1705.   }
  1706.  
  1707.   var box = su.elementGetBorderBox(target);
  1708.   var boxtop = box.top;
  1709.  
  1710.   if (target.tagName == 'TD') {
  1711.     boxtop = su.elementGetBorderBox(target.parentNode).top;
  1712.   }
  1713.  
  1714.   mgr.editPanel.style.top = (boxtop - mgr.HIGHLIGHT_EDIT_OFFSET +
  1715.     mgr.scrollPanel.scrollTop) + 'px';
  1716.  
  1717.   // Match widths first so content height is computed off wrapped content.
  1718.   mgr.editPanel.style.width = box.width + 'px';
  1719.   $('edit-field').style.width = box.width + 'px';
  1720.  
  1721.   if (mgr.isFloating()) {
  1722.     return;
  1723.   }
  1724.  
  1725.   var field = $('edit-field');
  1726.   var fieldHeight = field.scrollHeight;
  1727.  
  1728.   if (fieldHeight > box.height) {
  1729.     // Minimize the height of the field to get an accurate scrollHeight.
  1730.     field.style.height = '0px';
  1731.     fieldHeight = field.scrollHeight;
  1732.  
  1733.     // On the Mac the scrollHeight is reported without padding so account
  1734.     // for extra padding here.
  1735.     if (su.IS_MAC) {
  1736.       fieldHeight += 5;
  1737.       // If there is no content inside the field, its scrollHeight is reported
  1738.       // as 0 on the Mac, so account for that here.
  1739.       fieldHeight = Math.max(20, fieldHeight);
  1740.     }
  1741.     var contentHeight = Math.max(20, fieldHeight);
  1742.  
  1743.     // If there is more than a line of content and we're on the Mac, add a
  1744.     // pixel to fix a problem with the content resizing slightly when focused.
  1745.     if (su.IS_MAC && fieldHeight > 20) {
  1746.       contentHeight += 1;
  1747.     }
  1748.  
  1749.     mgr.editPanel.style.height = contentHeight + 'px';
  1750.     field.style.height = contentHeight + 'px';
  1751.     target.style.height = contentHeight + 'px';
  1752.     field.scrollTop = 0;
  1753.  
  1754.     // If the editor is visible, then set the underlying cell's HTML to be an
  1755.     // empty space, so the cell does not appear larger than the overlying
  1756.     // editor when one deletes several lines of text. The exception to this
  1757.     // is if we're entering a new attribute name.
  1758.     if (mgr.isEditing() && target.innerHTML.indexOf(
  1759.         su.translateString(mgr.ADD_ATTRIBUTE)) == -1) {
  1760.       su.setContent(target, ' ');
  1761.     }
  1762.   } else {
  1763.     var contentHeight = Math.max(15, box.height);
  1764.     mgr.editPanel.style.height = contentHeight + 'px';
  1765.     field.style.height = contentHeight + 'px';
  1766.     field.scrollTop = 0;
  1767.   }
  1768.   mgr.updateHighlightLayout();
  1769. };
  1770.  
  1771. /**
  1772.  * Calls updateEditorLayout inside a timeout to ensure that all rendering has
  1773.  * occurred before the field height is calculated. The onpaste operation is an
  1774.  * example of a routine which requires this approach.
  1775.  */
  1776. mgr.updateEditorLayoutTimeout = function() {
  1777.   window.setTimeout(function() {
  1778.     mgr.updateEditorLayout();
  1779.     mgr.floatEditorIfNecessary();
  1780.   }, 0);
  1781. };
  1782.  
  1783. /**
  1784.  * Takes an id of an HTML entity and returns a 3-element array of the entity
  1785.  * info that is encoded into that id. Id is expected to be in the form
  1786.  * TYPE_NUMBER_ATTNAME, such as value_4535_description.
  1787.  * @param {string} id String id to split apart.
  1788.  * @return {Array.<string>} An array of ID "parts".
  1789.  */
  1790. mgr.parseIdIntoParts = function(id) {
  1791.   var returnArray = [];
  1792.   var allParts = id.split('_');
  1793.  
  1794.   // Shift and store the "name" or "value" and the attribute number from the
  1795.   // front end of our split up string.
  1796.   returnArray.push(allParts.shift());
  1797.   returnArray.push(allParts.shift());
  1798.  
  1799.   // Join any remaining parts in the middle with an underscore, since
  1800.   // it's possible that we had attribute names with an underscore, such
  1801.   // as "value_12345_my_att_with_underscores".
  1802.   returnArray.push(allParts.join('_'));
  1803.   return returnArray;
  1804. };
  1805.  
  1806. /**
  1807.  * Adjusts the position of the 'floating editor', consisting of the edit
  1808.  * cell and a label.
  1809.  * @param {Boolean} opt_atTop True to pin the editor at the top of the
  1810.  * viewport.
  1811.  */
  1812. mgr.updateFloatingEditorLayout = function(opt_atTop) {
  1813.  
  1814.   var isTop;
  1815.  
  1816.   if (!mgr.isFloating()) {
  1817.     return;
  1818.   }
  1819.  
  1820.   var scrollTop = mgr.scrollPanel.scrollTop;
  1821.  
  1822.   if (typeof(opt_atTop) == 'boolean') {
  1823.     mgr.editPanel.floatOnTop = opt_atTop;
  1824.     isTop = opt_atTop;
  1825.   } else {
  1826.     isTop = mgr.editPanel.floatOnTop;
  1827.   }
  1828.  
  1829.   var offsetHeight = su.elementHeight(mgr.scrollPanel);
  1830.   var panelHeight = su.elementHeight(mgr.editPanel);
  1831.  
  1832.   var targetWidth = su.elementWidth(mgr.getEditorTarget());
  1833.   var targetHeight = su.elementHeight(mgr.getEditorTarget());
  1834.  
  1835.   var halfFieldOffset = mgr.FIELD_OFFSET / 2;
  1836.  
  1837.   // Adjust sizing as needed during resizing and floating of the editor.
  1838.   if (offsetHeight < (targetHeight + halfFieldOffset)) {
  1839.     mgr.editPanel.style.top = scrollTop - halfFieldOffset + 'px';
  1840.     mgr.editPanel.style.height = offsetHeight + 'px';
  1841.     $('edit-field').style.height = offsetHeight - mgr.FIELD_OFFSET + 'px';
  1842.   } else {
  1843.     // If we're scrolling toward the top when we tear off the editor then the
  1844.     // top of the reference tab will remain in that location (pinned to the
  1845.     // top), otherwise we'll be pinned to the bottom while we scroll.
  1846.     if (isTop) {
  1847.       // Pin the reference tab to the top edge.
  1848.       mgr.editPanel.style.top = scrollTop - halfFieldOffset + 'px';
  1849.     } else {
  1850.       mgr.editPanel.style.top = scrollTop - mgr.FIELD_OFFSET +
  1851.           (offsetHeight -
  1852.               (panelHeight - mgr.EDIT_FIELD_REFERENCE_TAB_HEIGHT)) -
  1853.           mgr.BORDER_OFFSET + 'px';
  1854.     }
  1855.  
  1856.     mgr.editPanel.style.height = (targetHeight + mgr.FIELD_OFFSET) + 'px';
  1857.     $('edit-field').style.height = (targetHeight) + 'px';
  1858.   }
  1859.  
  1860.   mgr.editPanel.style.width = (targetWidth) + 'px';
  1861.   $('edit-field').style.width = (targetWidth) + 'px';
  1862.  
  1863.   su.show('edit-field-reference-tab');
  1864. };
  1865.  
  1866. /**
  1867.  * Enable display of a 'floating tab' showing what field was being edited
  1868.  * when the user scrolls that field out of view.
  1869.  */
  1870. mgr.floatEditorIfNecessary = function() {
  1871.  
  1872.   if (mgr.isEditing()) {
  1873.     if (mgr.tree.isEditingName != true) {
  1874.  
  1875.       // If the scroll would cause the editor to be clipped then we need
  1876.       // to pop it out and let it follow the scroll activity. The issue
  1877.       // here is "viewport" visibility, which is bounded by the offset
  1878.       // height of the scroll panel.
  1879.       var field = mgr.getFocusedElement();
  1880.  
  1881.       var panel = mgr.editPanel;
  1882.       var scrollPanel = mgr.scrollPanel;
  1883.  
  1884.       var offset = su.elementY(scrollPanel);
  1885.  
  1886.       var scrollHeight = scrollPanel.scrollHeight;
  1887.       var offsetHeight = su.elementHeight(scrollPanel);
  1888.  
  1889.       var scrollTop = scrollPanel.scrollTop;
  1890.       var scrollBottom = scrollHeight - (scrollTop + offsetHeight);
  1891.  
  1892.       var fieldTop = su.elementY(field) - offset;
  1893.  
  1894.       var panelTop = su.elementY(panel) - offset;
  1895.       var panelHeight = su.elementHeight(panel);
  1896.  
  1897.       var scrollAbove = scrollTop + panelTop;
  1898.       var scrollBelow = scrollBottom + (offsetHeight - panelTop -
  1899.           panelHeight) - (mgr.FIELD_OFFSET / 2);
  1900.  
  1901.       var viewAbove = fieldTop - mgr.FIELD_OFFSET;
  1902.       var viewBelow = offsetHeight - (viewAbove + panelHeight) -
  1903.           mgr.FIELD_OFFSET - mgr.BORDER_OFFSET;
  1904.  
  1905.       // Don't pop the panel until it would clip at the top.
  1906.       if ((viewAbove > 0) && (viewBelow > 0)){
  1907.         if (mgr.isFloating()) {
  1908.           mgr.unfloatEditor();
  1909.         }
  1910.         return;
  1911.       }
  1912.  
  1913.       if (!mgr.isFloating()) {
  1914.         mgr.floatEditor(viewAbove <= 0);
  1915.       } else {
  1916.         mgr.updateFloatingEditorLayout(viewAbove <= 0);
  1917.       }
  1918.     }
  1919.   }
  1920. };
  1921.  
  1922. /**
  1923.  * Construct the 'floating tab' showing what field was being edited when
  1924.  * the user scrolls that field out of view.
  1925.  * @param {Boolean} atTop True to pin the editor at the top of the viewport.
  1926.  */
  1927. mgr.floatEditor = function(atTop) {
  1928.  
  1929.   mgr.updateFloatingEditorLayout(atTop);
  1930.   mgr.isFloating(true);
  1931.  
  1932.   // Make the edit field's overflow-y 'auto', so that it can accomodate large
  1933.   // values when floating.
  1934.   $('edit-field').style.overflowY = 'auto';
  1935. };
  1936.  
  1937. /**
  1938.  * Deconstruct the 'floating tab' showing what field was being edited when
  1939.  * the user scrolls that field out of view.
  1940.  */
  1941. mgr.unfloatEditor = function() {
  1942.  
  1943.   // Reattach to the edited field.
  1944.   su.hide('edit-field-reference-tab');
  1945.   mgr.isFloating(false);
  1946.  
  1947.   // Make the edit field's overflow-y 'hidden', so that the scroll bar
  1948.   // doesn't jump when editing in an inline fashion.
  1949.   $('edit-field').style.overflowY = 'hidden';
  1950.  
  1951.   mgr.updateEditorLayoutTimeout();
  1952. };
  1953.  
  1954. /**
  1955.  * Loops a component or group's units through the valid list.
  1956.  * @param {string} id The id number of the entity to toggle units on.
  1957.  */
  1958. mgr.toggleLengthUnits = function(id) {
  1959.   var entity = su.findEntity(id, mgr.rootEntity);
  1960.   var lengthUnits = comp.lengthUnits(entity);
  1961.   var attribute;
  1962.  
  1963.   // If there are any user-created length attributes attached to this entity,
  1964.   // then show a warning about changing your units.
  1965.   var dict = comp.getAttributes(entity);
  1966.   if (su.isValid(dict)) {
  1967.  
  1968.     // Loop across all of our attributes to determine if any of them are
  1969.     // in "default" length units. When we shift from default to a fixed value
  1970.     // we want to query only when the default isn't already that unit.
  1971.     var keys = su.getKeys(dict);
  1972.     var hasDefaultLengthAtts = false;
  1973.     for (var i = 0; i < keys.length; i++) {
  1974.       attribute = keys[i];
  1975.       if (su.isValid(comp.RESERVED[attribute])) {
  1976.         if (comp.RESERVED[attribute].unitGroup == 'LENGTH' &&
  1977.             su.notValid(comp.getAttributeFormulaUnits(entity, attribute))) {
  1978.           hasDefaultLengthAtts = true;
  1979.         }
  1980.       }
  1981.     }
  1982.  
  1983.     // If we found some default length attributes, then show our warning.
  1984.     if (hasDefaultLengthAtts == true) {
  1985.       if (mgr.confirmUnitsChange() == false) {
  1986.         su.stopPropagation();
  1987.         return;
  1988.       }
  1989.     }
  1990.   }
  1991.  
  1992.   var newUnits;
  1993.   if (lengthUnits == 'INCHES') {
  1994.     newUnits = 'CENTIMETERS';
  1995.   } else {
  1996.     newUnits = 'INCHES';
  1997.   }
  1998.   $('units-button').className = 'units-button-' + newUnits.toLowerCase();
  1999.   comp.setAttributeValue(entity, '_lengthunits', newUnits);
  2000.  
  2001.   // Since this will redraw our panel, we need to store the refocus rule.
  2002.   if (mgr.getFocusedElement()) {
  2003.     mgr.refocusIndex = mgr.FOCUS_CURRENT;
  2004.     mgr.refocusTarget = mgr.getFocusedElement().getAttribute('id');
  2005.   }
  2006.  
  2007.   comp.pushAttribute(entity, '_lengthunits', 'mgr.redraw');
  2008.   su.stopPropagation();
  2009. };
  2010.  
  2011. /**
  2012.  * Shows a confirm message to the user asking if they want to alter units.
  2013.  * Returns true if the user says "Yes", false if they say "No".
  2014.  * @return {boolean} true if the user wants to continue with units change.
  2015.  */
  2016. mgr.confirmUnitsChange = function() {
  2017.   var str = su.translateString('Warning: changing your units ' +
  2018.     'could result in your formulas not behaving as you expect. ' +
  2019.     'Are you sure you want to change them?');
  2020.   return confirm(str);
  2021. };
  2022.  
  2023. /**
  2024.  * Shows a confirm message to the user asking if they want to alter the units.
  2025.  * Returns true if the user says "Yes", false if they say "No".
  2026.  * @param {string} id The id number of the entity to toggle units on.
  2027.  * @param {string} attrName The attribute whose units are changing.
  2028.  */
  2029. mgr.handleFormulaUnitsChange = function(id, attrName) {
  2030.   var entity = su.findEntity(id, mgr.rootEntity);
  2031.   var pulldown = $('formulaunits-pulldown');
  2032.  
  2033.   var units = pulldown.value;
  2034.   var lengthUnits = comp.lengthUnits(entity);
  2035.   var currentUnits = comp.getAttributeFormulaUnits(entity, attrName);
  2036.  
  2037.   // If the attribute is using default units then we need to know whether
  2038.   // this is a true change or simply moving from implied to explicit value.
  2039.   if (su.notValid(currentUnits) && units != 'DEFAULT') {
  2040.     if (su.notEmpty(comp.getAttributeValue(entity, attrName))) {
  2041.       if (lengthUnits != units) {
  2042.         if (mgr.confirmUnitsChange() == false) {
  2043.           pulldown.selectedIndex = pulldown.getAttribute('undo-value');
  2044.           su.preventDefault();
  2045.           su.stopPropagation();
  2046.           return;
  2047.         } else {
  2048.           var confirmed = true;
  2049.         }
  2050.       }
  2051.     }
  2052.   }
  2053.  
  2054.   // If the user is switching from something like Inches to Default-Inches
  2055.   // it's not really a change, so we don't want to ask for confirmation.
  2056.   if (!confirmed && units == 'DEFAULT') {
  2057.     if ((currentUnits != undefined) && (lengthUnits != currentUnits)) {
  2058.       if (mgr.confirmUnitsChange() == false) {
  2059.         pulldown.selectedIndex = pulldown.getAttribute('undo-value');
  2060.         su.preventDefault();
  2061.         su.stopPropagation();
  2062.         return;
  2063.       }
  2064.     }
  2065.   }
  2066.  
  2067.   pulldown.setAttribute('undo-value', pulldown.selectedIndex)
  2068.   mgr.tree.storeOptions(false, $('formulaunits-pulldown'));
  2069. };
  2070.  
  2071. // ---------------------------------------------------------------------------
  2072. // AttributeTree Class
  2073. // ---------------------------------------------------------------------------
  2074.  
  2075. /**
  2076.  * Supports the overall tree structured display of the manager panel.
  2077.  * @param {string} id The global and internal ID of the attribute tree.
  2078.  * @constructor
  2079.  */
  2080. function AttributeTree(id) {
  2081.   this.id = id;
  2082. }
  2083.  
  2084. /**
  2085.  * The id/name this instance is referred to at the global level. This is used
  2086.  * in markup generated for the tree to allow event handlers to gain access to
  2087.  * the current tree instance quickly.
  2088.  * TOOD (idearat): remove this global reference requirement.
  2089.  * @type {string}
  2090.  */
  2091. AttributeTree.prototype.id = null;
  2092.  
  2093. /**
  2094.  * The last selected label, whose highlighting is often manipulated to help
  2095.  * display current selection focus to the user.
  2096.  * @type {Element}
  2097.  */
  2098. AttributeTree.prototype.lastLabelCell = null;
  2099.  
  2100. /**
  2101.  * The last entity attribute selected or manipulated. This tracks context for
  2102.  * attribute editing operations.
  2103.  * @type {Object}
  2104.  */
  2105. AttributeTree.prototype.lastAttributeSelected = null;
  2106.  
  2107. /**
  2108.  * The last entity object selected. Used to track context for editing and
  2109.  * highlighting operations.
  2110.  * @type {Object}
  2111.  */
  2112. AttributeTree.prototype.lastEntitySelected = null;
  2113.  
  2114. /**
  2115.  * True when an attribute name is being edited rather than the value.
  2116.  * @type {boolean}
  2117.  */
  2118. AttributeTree.prototype.isEditingName = false;
  2119.  
  2120. /**
  2121.  * Adds a blank option to the current entity being detailed.
  2122.  */
  2123. AttributeTree.prototype.addBlankOption = function() {
  2124.  
  2125.   var entity = su.findEntity(this.idToDetail, mgr.rootEntity);
  2126.   var attribute = this.attNameToDetail;
  2127.   var options = comp.getAttributeOptions(entity, attribute);
  2128.  
  2129.   var defaultLabel = su.translateString(mgr.DEFAULT_OPTION_LABEL);
  2130.   var defaultValue = su.translateString(mgr.DEFAULT_OPTION_VALUE);
  2131.  
  2132.   options += '&' + escape(defaultLabel) + '=' + escape(defaultValue);
  2133.  
  2134.   comp.setAttributeOptions(entity, attribute, options);
  2135.  
  2136.   su.setContent('options-panel', this.dumpOptionsTable(entity, attribute));
  2137.  
  2138.   var i = 1;
  2139.   while ($('option-label-' + i)) {
  2140.     i++;
  2141.   }
  2142.  
  2143.   $('option-label-' + (i - 1)).focus();
  2144.   $('option-label-' + (i - 1)).select();
  2145.   $('options-scroll').scrollTop = 9000;
  2146. };
  2147.  
  2148. /**
  2149.  * Adds a group of attributes to the current entity. This operation is
  2150.  * performed as a special choice within the addAttribute logic. The trigger
  2151.  * for this action is clicking on the header for an entire attribute set.
  2152.  * @param {string} groupName The name of the attribute group to add.
  2153.  */
  2154. AttributeTree.prototype.attachGroup = function(groupName) {
  2155.  
  2156.   var entity = this.lastEntitySelected;
  2157.   var attributesToPush = {};
  2158.  
  2159.   for (var attName in comp.RESERVED) {
  2160.     attributesToPush[attName] = {};
  2161.  
  2162.     var attribute = comp.RESERVED[attName];
  2163.     if ((attribute.group === groupName) &&
  2164.         !su.isValid(comp.getAttribute(entity, attName))) {
  2165.  
  2166.       var defaultValue = attribute.defaultValue;
  2167.       var value = su.ifEmpty(defaultValue, '');
  2168.  
  2169.       comp.setAttributeValue(entity, attName, value);
  2170.       comp.setAttributeFormula(entity, attName, '');
  2171.       comp.setAttributeLabel(entity, attName, attribute.label);
  2172.  
  2173.       attributesToPush[attName].value = value;
  2174.       attributesToPush[attName].label = attribute.label;
  2175.     }
  2176.   }
  2177.  
  2178.   // Push the new attribute information to Ruby/SketchUp and re-draw the UI
  2179.   // so we're sure we get all the new attributes into the entity's table.
  2180.   comp.pushAttributeSet(entity.id, attributesToPush, 'mgr.redraw', true);
  2181.  
  2182.   window.setTimeout(function() {
  2183.     mgr.tree.hideEditPanels();
  2184.   }, 0);
  2185. };
  2186.  
  2187. /**
  2188.  * Removes an attribute from the currently detailed entity.
  2189.  * @return {boolean} True if the attribute was successfully removed.
  2190.  */
  2191. AttributeTree.prototype.deleteAttribute = function() {
  2192.   var str;
  2193.  
  2194.   var entity = su.findEntity(this.idToDetail, mgr.rootEntity);
  2195.   var attribute = this.attNameToDetail;
  2196.  
  2197.   if (comp.RESERVED[attribute]) {
  2198.     if (comp.RESERVED[attribute].hasLiveValue) {
  2199.       str = su.translateString(
  2200.         'Are you sure you want to clear this attribute?');
  2201.       if (confirm(str)) {
  2202.         if (entity.attributeDictionaries) {
  2203.           this.hideEditPanels();
  2204.           this.hideDetailControls();
  2205.           mgr.doDeleteAttribute(entity, attribute);
  2206.           return true;
  2207.         }
  2208.       }
  2209.     }
  2210.   }
  2211.  
  2212.   str = su.translateString(
  2213.     'Are you sure you want to delete this attribute? ' +
  2214.     'Any formulas that refer to this will be broken.');
  2215.  
  2216.   if (confirm(str)) {
  2217.     this.hideEditPanels();
  2218.     this.hideDetailControls();
  2219.     mgr.doDeleteAttribute(entity, attribute);
  2220.     return true;
  2221.   }
  2222. };
  2223.  
  2224. /**
  2225.  * Generates an HTML 'TR' containing a specific attribute's data.
  2226.  * @param {Object} entity The entity containing the attribute.
  2227.  * @param {Object} attrName The attribute whose data we are to expose.
  2228.  * @param {number} rowCount The row number for the row being generated.
  2229.  * @return {string} The html output for a single attribute row.
  2230.  */
  2231. AttributeTree.prototype.dumpAttributeRow = function(entity,
  2232.                                                     attrName,
  2233.                                                     rowCount) {
  2234.   var label;
  2235.   var displayValue;
  2236.  
  2237.   // The base row class is tweaked to produce a striping effect for odd and
  2238.   // even cells.
  2239.   var oddEvenString = this.getRowClassByCount(rowCount);
  2240.   var rowClass = 'attribute-cell ' + oddEvenString;
  2241.  
  2242.   // If the attribute has user-level access then we also want to display a
  2243.   // small icon so the user is aware that configurable details exist. Do that
  2244.   // by adding a CSS class which exposes display of that icon.
  2245.   if ((su.isValid(comp.getAttributeAccess(entity, attrName)) &&
  2246.       comp.getAttributeAccess(entity, attrName) != 'NONE') ||
  2247.       su.isValid(comp.getAttributeFormulaUnits(entity, attrName, false))) {
  2248.     rowClass += ' details-icon-active';
  2249.   } else {
  2250.     rowClass += ' details-icon-inactive';
  2251.   }
  2252.  
  2253.   // The label is determined by the entity, followed by whether we've got a
  2254.   // standardized label (when the attribute is a known/reserved one).
  2255.   // Otherwise we default to the attrName itself.
  2256.   var attr = comp.getAttribute(entity, attrName);
  2257.   var reserved = comp.RESERVED[attrName];
  2258.   if (su.notEmpty(attr.label)) {
  2259.     label = attr.label;
  2260.   } else if (su.isValid(reserved)) {
  2261.     if (su.notEmpty(reserved.label)) {
  2262.       label = reserved.label;
  2263.     } else {
  2264.       label = attrName;
  2265.     }
  2266.     comp.setAttributeLabel(entity, attrName, label);
  2267.   } else {
  2268.     label = attrName;
  2269.   }
  2270.  
  2271.   // Provide for special coloring of the label.
  2272.   var labelStyle = '';
  2273.   if (su.isValid(reserved)) {
  2274.     if (su.notEmpty(reserved.color)) {
  2275.       labelStyle = 'color:' + reserved.color;
  2276.     }
  2277.   }
  2278.  
  2279.   // Determine the proper display style, again offering the first choice to
  2280.   // the attribute/entity information, then the reserved attribute list.
  2281.   var unitGroup = '';
  2282.   if (comp.getAttributeFormula(entity, attrName)) {
  2283.     rowClass += ' formula-result';
  2284.     if (comp.getAttributeFormula(entity, attrName).indexOf('$') == 0) {
  2285.       unitGroup = 'currency';
  2286.     }
  2287.   } else if (su.isValid(reserved)) {
  2288.     if (reserved.hasLiveValue == true) {
  2289.       rowClass += ' live-value-result';
  2290.     }
  2291.   }
  2292.  
  2293.   // Default to the unitGroup defined in our reserved list.
  2294.   if (su.isValid(reserved)) {
  2295.     unitGroup = su.ifEmpty(unitGroup, reserved.unitGroup);
  2296.   }
  2297.  
  2298.   if (comp.getAttributeError(entity, attrName).indexOf(
  2299.       'subformula-error') > -1) {
  2300.     displayValue = mgr.ERROR_PREFIX + '=' +
  2301.         comp.getAttributeError(entity, attrName);
  2302.   } else {
  2303.     displayValue = mgr.formatDisplayValue(entity, attrName);
  2304.   }
  2305.  
  2306.   var formula = comp.getAttributeFormula(entity, attrName)
  2307.   if (mgr.showFormulas == true && su.isEmpty(formula) == false) {
  2308.     displayValue = '=' + formula;
  2309.     displayValue = displayValue.replace(/\</g, '<');
  2310.     displayValue = displayValue.replace(/\>/g, '>');
  2311.     displayValue = displayValue.replace(/\//g,
  2312.         '/<span class="zero-width"> </span>');
  2313.     if (comp.getAttributeError(entity, attrName).indexOf(
  2314.         'error') > -1) {
  2315.       displayValue = mgr.ERROR_PREFIX + displayValue;
  2316.     }
  2317.   } else if (su.isEmpty(formula) == false) {
  2318.  
  2319.     // If a formula contains a boolean function and that formula contains
  2320.     // a boolean value of 0.0 or 1.0, then display the friendly TRUE or
  2321.     // FALSE text the same way that Google Spreadsheets does.
  2322.     //
  2323.     // Note: the match(/\bor\(/) will match or() but not floor()
  2324.     var lowerCaseFormula = formula.toLowerCase();
  2325.     if (lowerCaseFormula.indexOf('true') > -1 ||
  2326.         lowerCaseFormula.indexOf('false') > -1 ||
  2327.         lowerCaseFormula.indexOf('and(') > -1 ||
  2328.         lowerCaseFormula.indexOf('not(') > -1 ||
  2329.         lowerCaseFormula.match(/\bor\(/) != null ||
  2330.         lowerCaseFormula.indexOf('<') > -1 ||
  2331.         lowerCaseFormula.indexOf('>') > -1 ||
  2332.         lowerCaseFormula.indexOf('=') > -1) {
  2333.  
  2334.       if (displayValue == '1.0') {
  2335.         displayValue = 'TRUE';
  2336.       } else if (displayValue == '0.0') {
  2337.         displayValue = 'FALSE';
  2338.       }
  2339.     }
  2340.   }
  2341.  
  2342.   displayValue = mgr.insertSoftBreaks(displayValue);
  2343.  
  2344.   // With the various parts in place we can finally build the row itself.
  2345.   var arr = [];
  2346.  
  2347.   // Open a new row.
  2348.   arr.push('<tr>');
  2349.  
  2350.   // Output the label column markup.
  2351.   arr.push(
  2352.       '<td class="attribute-label ', oddEvenString, '" id="label_', entity.id,
  2353.       '_', attrName, '"', ' name="label_', rowCount, '"',
  2354.       ' valign="top" style="', labelStyle, '"',
  2355.       ' onclick="', this.id, '.handleAttributeClick(this, ',
  2356.       entity.id, ', \'', attrName, '\')"',
  2357.       ' ondblclick="mgr.resetValueCell();', this.id,
  2358.       '.editAttributeName(this, ', entity.id, ', \'', attrName, '\')"',
  2359.       ' onmousedown="mgr.storeSelectionTextRange()">',
  2360.       mgr.insertSoftBreaks(label),
  2361.       '</td>');
  2362.  
  2363.   // Output the value column markup.
  2364.   arr.push('<td id="value_', entity.id, '_', attrName, '"',
  2365.       ' name="field_', rowCount, '"',
  2366.       ' class="', rowClass, ' attribute-value"',
  2367.       ' ondblclick="mgr.resetValueCell();', this.id,
  2368.       '.editAttributeValue(this, ', entity.id, ', \'', attrName, '\');',
  2369.       '$(\'edit-field\').select();"',
  2370.       ' onclick="', this.id, '.handleAttributeClick(this, ',
  2371.       entity.id, ', \'', attrName, '\')" ',
  2372.       'onmousedown="mgr.storeSelectionTextRange()">',
  2373.       displayValue, ' ',
  2374.       '</td>');
  2375.  
  2376.   // Close off the row.
  2377.   arr.push('</tr>');
  2378.  
  2379.   var html = arr.join('');
  2380.   return html;
  2381. };
  2382.  
  2383. /**
  2384.  * Generates an HTML 'TABLE' containing a specific entity's data.
  2385.  * @param {Object} entity The entity to process.
  2386.  * @return {string} The html for the attribute table.
  2387.  */
  2388. AttributeTree.prototype.dumpAttributeTable = function(entity) {
  2389.  
  2390.   var collapseState;
  2391.   var branchClass;
  2392.  
  2393.   // If an entity was collapsed we'll generate output that maintains that
  2394.   // state during redraw/regeneration of the UI for that entity.
  2395.   // Note that we now show the root element as visible if it has not
  2396.   // been explicitly collapsed.
  2397.   if (su.isEmpty(comp.getAttributeValue(entity, '_iscollapsed'))) {
  2398.     if (mgr.rootEntity == entity) {
  2399.       collapseState = 'visible';
  2400.     } else {
  2401.       collapseState = 'collapsed';
  2402.     }
  2403.   } else if (comp.getAttributeValue(entity, '_iscollapsed') == 'false') {
  2404.     collapseState = 'visible';
  2405.   } else {
  2406.     collapseState = 'collapsed';
  2407.   }
  2408.  
  2409.   var arr = [];
  2410.  
  2411.   // Get our length units for this entity, defaulting to inches.
  2412.   var lengthUnits = comp.lengthUnits(entity);
  2413.  
  2414.   arr.push('<div class="tree-leaf-', collapseState, '">',
  2415.     '<div id="attribute-head-', entity.id, '"',
  2416.     ' class="attribute-head-', collapseState, '"',
  2417.     ' onclick="', this.id, '.toggleCollapse(this, \'', entity.id, '\')"', '>',
  2418.     '<span class="units-button-panel"><span onclick="mgr.toggleLengthUnits(\'',
  2419.     entity.id, '\')" ', 'id="units-button" class="units-button-',
  2420.     lengthUnits.toLowerCase(), '" title="',
  2421.     su.translateString('Toggle Metric'), '"> </span></span>',
  2422.     '<span onclick="', 'mgr.toggleSelection(\'', entity.id, '\');"',
  2423.     ' ondblclick="', 'mgr.renameEntity(', entity.id, ', ',
  2424.     su.quote(entity.name), ')"',
  2425.     ' title="' + su.translateString('Double click to rename') + '"',
  2426.     ' class="attribute-head-text icon-', entity.typename.toLowerCase(), '">',
  2427.     entity.name,
  2428.     '</span>',
  2429.     '</div>',
  2430.     '<div id="attribute-table-wrapper-', entity.id, '"',
  2431.     ' class="attribute-table-wrapper">',
  2432.     '<table class="attribute-table" cellspacing="0">'
  2433.   );
  2434.  
  2435.   // Locate the specific attribute library for components, the rest will be
  2436.   // ignored for now.
  2437.   for (var libraryName in entity.attributeDictionaries) {
  2438.     if (libraryName != comp.DICTIONARY) {
  2439.       continue;
  2440.     }
  2441.   }
  2442.  
  2443.   // Make sure we found what we were looking for in terms of attribute set.
  2444.   if (su.notValid(libraryName)) {
  2445.     su.raise(su.translateString('Unable to locate component library name.'));
  2446.     return;
  2447.   }
  2448.  
  2449.   // Capture the library itself, otherwise signal a problem.
  2450.   var thisLib = entity.attributeDictionaries[libraryName];
  2451.   if (su.notValid(thisLib)) {
  2452.     su.raise(su.translateString(
  2453.         'Unable to locate component attribute dictionary.'));
  2454.     return;
  2455.   }
  2456.  
  2457.   // Keep track of the group name of each attribute, so we can output a
  2458.   // subhead for each new group that we encounter.
  2459.   var lastGroup = '';
  2460.  
  2461.   // Track rowCount so we can display stripped table rows in the output.
  2462.   var rowCount = 1;
  2463.  
  2464.   for (var attName in thisLib) {
  2465.  
  2466.     // Do not show attributes that start with an underscore, these are used
  2467.     // internally for maintaining UI state.
  2468.     if (attName.indexOf('_') == 0) {
  2469.       continue;
  2470.     }
  2471.  
  2472.     rowCount++;
  2473.  
  2474.     // Show a subhead for each new group that we encounter.
  2475.     var attr = comp.RESERVED[attName];
  2476.     var thisGroup = su.isValid(attr) ? attr.group : 'Custom';
  2477.  
  2478.     // If we change groups output a new section subheading.
  2479.     if (thisGroup != lastGroup) {
  2480.       arr.push('<tr><td class="attribute-subhead-label" colspan="2">',
  2481.         su.translateString(thisGroup),
  2482.         '</td></tr>');
  2483.       lastGroup = thisGroup;
  2484.     }
  2485.  
  2486.     // Process the attribute row, injecting that input into our table.
  2487.     arr.push(this.dumpAttributeRow(entity, attName, rowCount));
  2488.   }
  2489.  
  2490.   // Generate an add attribute row as the last row of the overall table.
  2491.   var rowClass = this.getRowClassByCount(rowCount + 1);
  2492.  
  2493.   arr.push('<tr>',
  2494.       '<td class="attribute-label ', rowClass, '"',
  2495.       ' onclick="', this.id, '.editAttributeName(this, \'', entity.id, '\')">',
  2496.       '<div id="add-attribute-link-', entity.id,
  2497.       '" class="add-attribute-link"><nobr>',
  2498.       su.translateString(mgr.ADD_ATTRIBUTE), '</nobr></div>',
  2499.       '</td>',
  2500.       '<td class="attribute-cell ', rowClass, '"> </td>',
  2501.       '</tr>');
  2502.  
  2503.   // Close the overall table and the divs which handle collapse/expand.
  2504.   arr.push('</table></div></div>');
  2505.  
  2506.   // Dump out child entity(s) beneath this entity. We currently limit this to
  2507.   // one level of child content.
  2508.   if (su.isValid(entity.subentities)) {
  2509.     var len = entity.subentities.length;
  2510.     for (var i = 0; i < len; i++) {
  2511.       if (i == (len - 1)) {
  2512.         branchClass = 'tree-branch-l';
  2513.       } else {
  2514.         branchClass = 'tree-branch-i';
  2515.       }
  2516.  
  2517.       // Wrap each child entity we add in a wrapper with child entity ID.
  2518.       arr.push('<div id="', entity.subentities[i].id, '"',
  2519.         ' class="', branchClass, '">',
  2520.         this.dumpAttributeTable(entity.subentities[i]),
  2521.         '</div>');
  2522.     }
  2523.   }
  2524.  
  2525.   var html = arr.join('');
  2526.   return html;
  2527. };
  2528.  
  2529. /**
  2530.  * Generate HTML for configuration options related to an entity/attribute pair.
  2531.  * @param {Object} entity The entity containing the attribute.
  2532.  * @param {Object} attrName The attribute whose data we are to expose.
  2533.  * @return {string} The html for the options table.
  2534.  */
  2535. AttributeTree.prototype.dumpOptionsTable = function(entity, attrName) {
  2536.   var options = comp.getAttributeOptions(entity, attrName);
  2537.   if (su.isString(options)) {
  2538.     options = su.unescapeHTML(options);
  2539.   }
  2540.  
  2541.   var arr = [];
  2542.   arr.push('<div id="options-scroll">',
  2543.     '<table width="100%" cellpadding="0" cellspacing="0"',
  2544.     ' class="options-table">',
  2545.     '<tr><td class="options-head">', su.translateString('List Option'),
  2546.     '</td>', '<td class="options-head">', su.translateString('Value'),
  2547.     '</td></tr>');
  2548.  
  2549.   // Gather the information we need to format the underlying value into a
  2550.   // display value based on the attibute's formula units. We look first for
  2551.   // a pulldown control, in case the user just changed their units in the
  2552.   // control before committing the change.
  2553.   var units;
  2554.  
  2555.   // If an explicit unit is selected, then use that instead of the default one.
  2556.   // This allows us to refresh the options list in the currently selected
  2557.   // units even before the change is committed.
  2558.   if (su.isValid($('formulaunits-pulldown'))) {
  2559.     units = $('formulaunits-pulldown').value;
  2560.     if (units == 'DEFAULT') {
  2561.       if (su.isValid(comp.RESERVED[attrName])) {
  2562.         if (comp.RESERVED[attrName].unitGroup == 'LENGTH') {
  2563.           units = su.ifEmpty(comp.lengthUnits(entity), units);
  2564.         }
  2565.       } else {
  2566.         units = 'STRING';
  2567.       }
  2568.     }
  2569.   }
  2570.   var units = su.ifEmpty(units,
  2571.       comp.getAttributeFormulaUnits(entity, attrName, true));
  2572.  
  2573.   var rowID = 0;
  2574.   if (su.notEmpty(options)) {
  2575.     var valuePairs = options.split('&');
  2576.     var len = valuePairs.length;
  2577.     for (var i = 0; i < len; i++) {
  2578.       var pair = valuePairs[i];
  2579.       if (su.notEmpty(pair) && pair != 'undefined') {
  2580.         rowID++;
  2581.         var nameValueArray = pair.split('=');
  2582.  
  2583.         // Format the name and value into strings that will
  2584.         // display nicely in an HTML text box.
  2585.         var name = unescape(nameValueArray[0]);
  2586.         var value = unescape(nameValueArray[1]);
  2587.         if (value.indexOf('=') != 0 &&
  2588.             value != mgr.DEFAULT_OPTION_VALUE) {
  2589.           value = conv.fromBase(value, units);
  2590.           value = conv.format(value, units, 6);
  2591.         }
  2592.         name = name.replace(/\"/gi, '"');
  2593.         value = value.replace(/\"/gi, '"');
  2594.  
  2595.         arr.push('<tr>',
  2596.             '<td class="options-label">',
  2597.             '<input id="option-label-', rowID, '"',
  2598.             ' class="options-field" ',
  2599.             ' value="', name, '"',
  2600.             ' onfocus="this.setAttribute(\'undo-value\', this.value)"',
  2601.             ' onblur="', this.id, '.storeOptions(true, this)"',
  2602.             '>',
  2603.             '</td>',
  2604.             '<td class="options-value">',
  2605.             '<input id="option-value-', rowID, '"',
  2606.             ' class="options-field"',
  2607.             ' value="', value, '"',
  2608.             ' onfocus="this.setAttribute(\'undo-value\', this.value)"',
  2609.             ' onblur="', this.id, '.storeOptions(false, this)"',
  2610.             '>',
  2611.             '</td></tr>');
  2612.       }
  2613.     }
  2614.   }
  2615.  
  2616.   // Build a final 'add option' row for the table.
  2617.   arr.push('<tr><td class="add-option-link"',
  2618.       ' onclick="', this.id, '.addBlankOption()"',
  2619.       ' colspan="2">' + su.translateString('Add option') + '</td></tr>',
  2620.       '</table>');
  2621.  
  2622.   // Note this field isn't identified and is positioned off screen
  2623.   // above the viewport so it's effectively never accessible except by
  2624.   // having a keyboard action cause it to focus. It's here to help us trap
  2625.   // tabbing events rather than losing focus to an element we don't control.
  2626.   arr.push('<input type="text" style="position: absolute;top: -500px;"',
  2627.       ' onfocus="', this.id, '.addBlankOption()">');
  2628.  
  2629.   // Close off the 'options-scroll' div.
  2630.   arr.push('</div>');
  2631.  
  2632.   var html = arr.join('');
  2633.   return html;
  2634. };
  2635.  
  2636. /**
  2637.  * Handles requests to edit an attribute name. This is triggered by a
  2638.  * double-click action on an attribute label cell in the user interface.
  2639.  * @param {Element} target The target element for the (dbl)click.
  2640.  * @param {string} entityID The ID of the entity which owns the attribute.
  2641.  * @param {string} attrName The name of the attribute to be renamed.
  2642.  */
  2643. AttributeTree.prototype.editAttributeName = function(target, entityID,
  2644.     attrName) {
  2645.  
  2646.   // Stop floating.
  2647.   mgr.unfloatEditor();
  2648.  
  2649.   // Find the entity object by searching through our root entity.
  2650.   var entity = su.findEntity(entityID, mgr.rootEntity);
  2651.   if (su.notValid(entity)) {
  2652.     su.raise(su.translateString('Could not find entity: ') + entityID);
  2653.     return;
  2654.   }
  2655.  
  2656.   // Can't rename reserved attributes, so check that dictionary.
  2657.   if (su.isValid(comp.RESERVED[attrName])) {
  2658.     mgr.tree.hideEditPanels();
  2659.     mgr.tree.highlight();
  2660.     alert(su.translateString('You cannot rename reserved attributes.'));
  2661.     return;
  2662.   }
  2663.  
  2664.   var attr = comp.getAttribute(entity, attrName);
  2665.   var field = $('edit-field');
  2666.  
  2667.   // Set the edit field's value to the current attribute name or prompt.
  2668.   if (su.notEmpty(attrName)) {
  2669.     var label = su.isValid(attr) ? attr.label : attrName;
  2670.     comp.setAttributeLabel(entity, attrName, label);
  2671.     field.value = label;
  2672.   } else {
  2673.     field.value = su.translateString(mgr.ENTER_NAME_STRING);
  2674.   }
  2675.  
  2676.   mgr.hideHighlight();
  2677.  
  2678.   // Update our current selection context so any edit/redisplay will know
  2679.   // where we were focused etc.
  2680.   mgr.setFocusedElement(target);
  2681.   this.lastEntitySelected = entity;
  2682.   this.lastAttributeSelected = attr;
  2683.   this.isEditingName = true;
  2684.  
  2685.   // Display/focus the actual editing controls.
  2686.   this.showEditPanel(target);
  2687.  
  2688.   if (su.isEmpty(attrName)) {
  2689.     this.showListPanel(entity);
  2690.   }
  2691.  
  2692.   field.select();
  2693.  
  2694.   // We need to stop event propagation here to properly support clicking
  2695.   // on the add attribute link in IE.
  2696.   su.stopPropagation();
  2697. };
  2698.  
  2699. /**
  2700.  * Handles requests to edit an attribute value. This is triggered by a
  2701.  * double-click action on an attribute value cell in the user interface.
  2702.  * @param {Element} target The target element for the (dbl)click.
  2703.  * @param {string} opt_entityID The ID of the entity owning the attribute.
  2704.  * @param {string} opt_attrName The name of the attribute to be updated.
  2705.  */
  2706. AttributeTree.prototype.editAttributeValue = function(target, opt_entityID,
  2707.     opt_attrName) {
  2708.   if (su.isEmpty(opt_entityID)) {
  2709.     var entity = this.lastEntitySelected || mgr.rootEntity;
  2710.   } else {
  2711.     var entity = su.findEntity(opt_entityID, mgr.rootEntity);
  2712.   }
  2713.  
  2714.   if (su.notValid(entity)) {
  2715.     su.raise(su.translateString('Could not find entity: ') + opt_entityID);
  2716.     return;
  2717.   }
  2718.  
  2719.   if (su.isEmpty(opt_attrName)) {
  2720.     var id = target.getAttribute('id');
  2721.     if (su.isEmpty(id)) {
  2722.       return;
  2723.     }
  2724.     var parts = mgr.parseIdIntoParts(id);
  2725.     if (parts.length < 3) {
  2726.       return;
  2727.     }
  2728.     var attrName = parts[2];
  2729.   } else {
  2730.     var attrName = opt_attrName;
  2731.   }
  2732.  
  2733.   var attr = comp.getAttribute(entity, attrName);
  2734.   if (su.notValid(attr)) {
  2735.     su.raise(su.translateString('Could not find attribute: ') + opt_attrName);
  2736.     return;
  2737.   }
  2738.  
  2739.   var field = $('edit-field');
  2740.  
  2741.   // We edit formula or value in that order (first non-empty/null value).
  2742.   var formula = comp.getAttributeFormula(entity, attrName);
  2743.   if (su.notEmpty(formula)) {
  2744.     field.value = '=' + su.unescapeHTML(formula);
  2745.   } else {
  2746.     var value = comp.getAttributeFormattedValue(entity, attrName,
  2747.         mgr.DEFAULT_EDIT_DECIMAL_PLACES);
  2748.     field.value = su.unescapeHTML(value);
  2749.   }
  2750.  
  2751.   // If the user clicked on the label cell to select this attribute, then the
  2752.   // target which gets passed in is incorrect. Therefore, recalculate the
  2753.   // correct target.
  2754.   target = $(target.getAttribute('id').replace(/label_/, 'value_'));
  2755.  
  2756.   // Update our current selection context so any edit/redisplay will know
  2757.   // where we were focused etc.
  2758.   mgr.setFocusedElement(target);
  2759.   this.lastEntitySelected = entity;
  2760.   this.lastAttributeSelected = attr;
  2761.   this.isEditingName = false;
  2762.  
  2763.   if (attrName != 'scaletool') {
  2764.     this.showEditPanel(target);
  2765.     this.highlight(target);
  2766.     field.focus();
  2767.  
  2768.     su.selectFromTo(field, field.value.length, field.value.length);
  2769.  
  2770.     // On the Mac, the selectFromTo fails if there is nothing in the field,
  2771.     // so handle that here.
  2772.     if (su.isEmpty(field.value)) {
  2773.       field.select();
  2774.     }
  2775.  
  2776.     // Now that we've moved the text selection to the end of the field,
  2777.     // set out scroll to 0 so we don't see flashing on the PC.
  2778.     field.scrollTop = 0;
  2779.   } else {
  2780.     this.showDetailPanel();
  2781.   }
  2782.  
  2783.   var statusStr = '<b>' + su.truncate(
  2784.       comp.getAttributeLabel(entity, attrName), 40) + '</b>';
  2785.  
  2786.   var reserved = comp.RESERVED[attrName];
  2787.   if (su.isValid(reserved)) {
  2788.     statusStr += ' · ' + su.translateString(reserved.summary);
  2789.     if (su.notEmpty(formula)) {
  2790.       statusStr += '<br/>=' + comp.getAttributeFormula(entity, attrName);
  2791.     }
  2792.     mgr.setStatusBar(statusStr, attrName);
  2793.   } else {
  2794.     statusStr += ' · ' + su.translateString('Custom attribute.');
  2795.     if (su.notEmpty(formula)) {
  2796.       statusStr += '<br/>=' + comp.getAttributeFormula(entity, attrName);
  2797.     }
  2798.     mgr.setStatusBar(statusStr, 'custom');
  2799.   }
  2800.  
  2801.   // Update our editor height to match the height of its content. This is to
  2802.   // show long formulas properly at the moment that one starts editing.
  2803.   mgr.updateEditorLayout();
  2804. };
  2805.  
  2806. /**
  2807.  * Activates the buttons which support saving or refreshing the user interface
  2808.  * when an editing operation is in progress and has changed a value.
  2809.  */
  2810. AttributeTree.prototype.enableEditButtons = function() {
  2811.   su.enable('applyButton');
  2812.   su.enable('refreshButton');
  2813. };
  2814.  
  2815. /**
  2816.  * Returns a class which can be used to help display striping for table or
  2817.  * list cells based on the rowCount provided (odd or even).
  2818.  * @param {number} rowCount The row number to use for class computation.
  2819.  * @return {string} The CSS class name.
  2820.  */
  2821. AttributeTree.prototype.getRowClassByCount = function(rowCount) {
  2822.   if ((rowCount % 2) == 0) {
  2823.     return ' even ';
  2824.   } else {
  2825.     return ' odd ';
  2826.   }
  2827. };
  2828.  
  2829. /**
  2830.  * Responds to notifications that an edit operation has potentially occurred.
  2831.  * When data has changed in the edit field this method will ensure that it is
  2832.  * pushed to SketchUp via the Ruby bridge.
  2833.  * @param {Element} opt_formField The field serving as the edit field.
  2834.  * @param {Element} opt_target An optional target element being edited.
  2835.  * @param {number} opt_index A focus index, next, previous, or current.
  2836.  * @param {boolean} opt_editor Should the editor display on completion.
  2837.  * @return {boolean} True if there were edits which were found.
  2838.  */
  2839. AttributeTree.prototype.handleEdit = function(opt_formField, opt_target,
  2840.     opt_index, opt_editor) {
  2841.  
  2842.   var name;
  2843.   var label;
  2844.  
  2845.   window.setTimeout(function() {
  2846.     mgr.tree.hideEditPanels();
  2847.   }, 0);
  2848.  
  2849.   mgr.isFloating(false);
  2850.  
  2851.   // Set any refocusing parameters on the manager since the tree instance
  2852.   // will be replaced during redraw and we'll lose any instance data.
  2853.   mgr.refocusTarget = su.isValid(opt_target) ?
  2854.       opt_target.getAttribute('id') :
  2855.       opt_target;
  2856.   mgr.refocusIndex = su.ifInvalid(opt_index, mgr.FOCUS_CURRENT);
  2857.   mgr.refocusEditor = su.ifInvalid(opt_editor, true);
  2858.  
  2859.   var entity = this.lastEntitySelected;
  2860.   if (su.isValid(this.lastAttributeSelected)) {
  2861.     if (su.notEmpty(this.lastAttributeSelected.label)) {
  2862.       name = this.lastAttributeSelected.label.toLowerCase();
  2863.     } else {
  2864.       name = this.lastAttributeSelected.id;
  2865.     }
  2866.   } else {
  2867.     name = undefined;
  2868.   }
  2869.  
  2870.   // Convert to element as needed.
  2871.   var field = $(opt_formField || $('edit-field'));
  2872.  
  2873.   var defaultValue = '';
  2874.   var dirty = false;
  2875.   var lastAttributeLabel;
  2876.   if (su.isValid(this.lastAttributeSelected)) {
  2877.     lastAttributeLabel = this.lastAttributeSelected.label;
  2878.   }
  2879.  
  2880.   if (this.isEditingName) {
  2881.  
  2882.     field.value = field.value.replace(/[\r\n]/gi, '');
  2883.     field.value = su.trimWhitespace(field.value);
  2884.  
  2885.     var attrName = field.value.toLowerCase();
  2886.  
  2887.     if (su.isEmpty(attrName)) {
  2888.       alert(su.translateString('Attribute names cannot be empty. ' +
  2889.           'Please enter a different name.'));
  2890.       return this.refocusAfterNamingError();
  2891.     } else if ((comp.hasAttribute(entity, attrName)) &&
  2892.         lastAttributeLabel.toLowerCase() != attrName.toLowerCase()) {
  2893.       alert(su.translateString('Duplicate attribute name. ' +
  2894.           'Please enter a different name.'));
  2895.       return this.refocusAfterNamingError();
  2896.     } else if ((field.value == su.translateString(mgr.ENTER_NAME_STRING)) ||
  2897.         field.value.length == 0) {
  2898.       mgr.resetValueCell();
  2899.     } else if (field.value.indexOf(' ') > -1) {
  2900.  
  2901.       alert(su.translateString('Attribute names cannot contain spaces. ' +
  2902.           'Please enter a different name.'));
  2903.       return this.refocusAfterNamingError();
  2904.  
  2905.     } else if (field.value.search(/\W/) > -1) {
  2906.  
  2907.       alert(su.translateString('Attribute names can only contain letters and numbers. ' +
  2908.           'Please enter a different name.'));
  2909.       return this.refocusAfterNamingError();
  2910.  
  2911.     } else if (field.value.indexOf('_') == 0) {
  2912.  
  2913.       alert(su.translateString('Attribute names cannot begin with an underscore. ' +
  2914.           'Please enter a different name.'));
  2915.       return this.refocusAfterNamingError();
  2916.  
  2917.     } else if (field.value.match(/^\d/) !== null) {
  2918.  
  2919.       alert(su.translateString('Attribute names cannot begin with an number. ' +
  2920.           'Please enter a different name.'));
  2921.       return this.refocusAfterNamingError();
  2922.  
  2923.     } else if (field.value.match(/^(true|false)$/i) != null) {
  2924.  
  2925.       alert(su.translateString('You may not name an attribute "true" or "false". ' +
  2926.           'Please enter a different name.'));
  2927.       return this.refocusAfterNamingError();
  2928.  
  2929.     } else if (name == field.value.toLowerCase()) {
  2930.  
  2931.       if (su.notEmpty(comp.RESERVED[attrName])) {
  2932.         label = comp.RESERVED[attrName].label;
  2933.       } else {
  2934.         label = field.value;
  2935.       }
  2936.  
  2937.       comp.setAttributeLabel(entity, attrName, label);
  2938.       mgr.redraw();
  2939.  
  2940.     } else if (name != undefined) {
  2941.  
  2942.       var newLabel = field.value;
  2943.       var newName = field.value.toLowerCase();
  2944.  
  2945.       dirty = true;
  2946.  
  2947.       // In this case, we're about to rename, so construct
  2948.       // a refocusTarget string manually and always place the highlight
  2949.       // onto the newly created field.
  2950.       mgr.refocusTarget = 'value_' + entity.id + '_' + attrName;
  2951.       mgr.refocusIndex = mgr.FOCUS_CURRENT;
  2952.       mgr.refocusEditor = false;
  2953.  
  2954.       comp.pushRename(entity, name, newName, newLabel, 'mgr.redraw');
  2955.  
  2956.     } else if (comp.hasAttribute(entity, attrName)) {
  2957.  
  2958.       alert(su.translateString('The attribute already exists.'));
  2959.       return this.refocusAfterNamingError();
  2960.  
  2961.     } else {
  2962.  
  2963.       if (comp.RESERVED[attrName]) {
  2964.         label = comp.RESERVED[attrName].label;
  2965.         defaultValue = comp.RESERVED[attrName].defaultValue;
  2966.       } else {
  2967.         label = field.value;
  2968.       }
  2969.  
  2970.       var value = su.ifEmpty(defaultValue, '');
  2971.  
  2972.       comp.setAttributeValue(entity, attrName, value);
  2973.       comp.setAttributeFormula(entity, attrName, '');
  2974.       comp.setAttributeLabel(entity, attrName, label);
  2975.  
  2976.       // In this case, we're about to create a new attribute, so construct
  2977.       // a refocusTarget string manually and always place the highlight
  2978.       // onto the newly created field.
  2979.       mgr.refocusTarget = 'value_' + entity.id + '_' + attrName;
  2980.       mgr.refocusIndex = mgr.FOCUS_CURRENT;
  2981.       mgr.refocusEditor = false;
  2982.  
  2983.       dirty = true;
  2984.       comp.pushAttribute(entity, attrName, 'mgr.redraw');
  2985.     }
  2986.  
  2987.   } else {
  2988.  
  2989.     // Editing value.
  2990.  
  2991.     var valueHasCarriageReturns = field.value.search(/[\r\n]/gi);
  2992.     field.value = field.value.replace(/[\r\n]/gi, '');
  2993.  
  2994.     if (field.value.indexOf('=') == 0) {
  2995.  
  2996.       // Content represents an explicit formula, strip leading prefix.
  2997.       var rawFormula = field.value.substring(1);
  2998.  
  2999.       var attrName = field.value.toLowerCase();
  3000.  
  3001.       if (comp.getAttributeFormula(entity, name) != rawFormula) {
  3002.         mgr.setValueCell(entity, name, field.value);
  3003.         var thisAtt = comp.getAttribute(entity, attrName);
  3004.         comp.setAttributeFormula(entity, name, rawFormula)
  3005.         comp.setAttributeLabel(entity, name,
  3006.             this.lastAttributeSelected.label);
  3007.         this.enableEditButtons();
  3008.         dirty = true;
  3009.         comp.pushAttribute(entity, name, 'mgr.redraw');
  3010.         mgr.setValueCell(entity, name, mgr.APPLYING_ATTRIBUTE_MESSAGE);
  3011.  
  3012.       } else {
  3013.  
  3014.         mgr.resetValueCell();
  3015.  
  3016.       }
  3017.     } else {
  3018.  
  3019.       // Start with what the user entered.
  3020.       var enteredValue = field.value;
  3021.  
  3022.       // Content is a raw value, not a formula.
  3023.       var hasLiveValue = false;
  3024.  
  3025.       // Determine whether this attribute has a "LiveValue", meaning it is
  3026.       // a value such as LENX that is derived from SketchUp. Live valued
  3027.       // attributes will be displayed in gray text unless a formula is set
  3028.       // on them.
  3029.       if (comp.RESERVED[name]) {
  3030.         if (comp.RESERVED[name].hasLiveValue == true) {
  3031.           hasLiveValue = true;
  3032.         }
  3033.       } else {
  3034.         // If the attibute has no formula units, then attempt to determine the
  3035.         // unit group from the string the user entered.
  3036.         var foundUnits = comp.getAttributeFormulaUnits(entity, name);
  3037.         if (su.isEmpty(foundUnits)) {
  3038.           foundUnits = conv.recognizeUnits(enteredValue, true);
  3039.           if (su.isValid(foundUnits)) {
  3040.             comp.setAttributeFormulaUnits(entity, name, foundUnits);
  3041.           }
  3042.         }
  3043.       }
  3044.  
  3045.       // Take the entered value and turn it into the appropriate base
  3046.       // units. (For example, lengths are always stored in inches, regardless
  3047.       // of the unit they are displayed in.)
  3048.       enteredValue = comp.parseToBase(enteredValue, entity, name);
  3049.  
  3050.       if (comp.getAttributeValue(entity, name) !== field.value ||
  3051.           su.isValid(comp.getAttributeFormula(entity, name))) {
  3052.         comp.setAttributeValue(entity, name, enteredValue);
  3053.         comp.setAttributeFormula(entity, name, undefined);
  3054.         comp.setAttributeLabel(entity, name,
  3055.             this.lastAttributeSelected.label);
  3056.         this.enableEditButtons();
  3057.         dirty = true;
  3058.         comp.pushAttribute(entity, name, 'mgr.redraw');
  3059.       }
  3060.       mgr.resetValueCell();
  3061.     }
  3062.   }
  3063.  
  3064.   // When dirty is true it means there's an async call in progress to Ruby
  3065.   // which will deal with updating focus etc. Otherwise we have to handle any
  3066.   // specifics around focus/editor display locally.
  3067.   if (!dirty) {
  3068.     if (su.isValid(opt_index) || opt_editor) {
  3069.       window.setTimeout(function() {
  3070.         mgr.refocus();
  3071.       }, 0);
  3072.     }
  3073.   }
  3074.  
  3075.   return dirty;
  3076. };
  3077.  
  3078. /**
  3079.  * Responds to notifications that a keydown event has occurred. Key down
  3080.  * events are used to process navigation keys that we don't want to end up in
  3081.  * the target field such as Tab and Return.
  3082.  * @param {Element} target The element which received the event.
  3083.  * @param {Event} evt The native keydown event.
  3084.  * @param {number} opt_manualKeycode An optional alternative keycode to force.
  3085.  * @return {boolean} True so the event default operation continues.
  3086.  */
  3087. AttributeTree.prototype.handleKeyDown = function(target, evt,
  3088.     opt_manualKeycode) {
  3089.  
  3090.   var keycode = su.ifInvalid(opt_manualKeycode, su.getKeyCode(evt));
  3091.  
  3092.   mgr.storeSelectionTextRange();
  3093.  
  3094.   switch (keycode) {
  3095.     case su.ESCAPE_KEY:
  3096.       su.stopPropagation(evt);
  3097.       su.preventDefault(evt);
  3098.       if (mgr.isDetailing()) {
  3099.         var ev = evt || window.event;
  3100.         var target = ev.target || ev.srcElement;
  3101.         var ancestor = target.parentNode;
  3102.         if (ancestor && ancestor.className == 'options-label') {
  3103.           if (target.value == su.translateString(mgr.DEFAULT_OPTION_LABEL)) {
  3104.             target.blur();
  3105.             break;
  3106.           }
  3107.           target.value = target.getAttribute('undo-value') || '';
  3108.           target.select();
  3109.         } else if (ancestor && ancestor.className == 'options-value') {
  3110.           if (target.value == su.translateString(mgr.DEFAULT_OPTION_VALUE)) {
  3111.             target.blur();
  3112.             break;
  3113.           }
  3114.           target.value = target.getAttribute('undo-value') || '';
  3115.           target.select();
  3116.         } else {
  3117.           mgr.tree.hideDetailPanel();
  3118.         }
  3119.       } else if (mgr.isEditing()) {
  3120.         mgr.resetValueCell();
  3121.         mgr.tree.hideEditPanels();
  3122.         mgr.tree.highlight();
  3123.       } else {
  3124.         mgr.hideHighlight();
  3125.         mgr.setFocusedElement(null);
  3126.         mgr.refocusIndex = null;
  3127.         mgr.refocusTarget = null;
  3128.         mgr.tree.hideDetailControls();
  3129.       }
  3130.       break;
  3131.     case su.ARROW_UP_KEY:
  3132.       if (!mgr.isEditing() && !mgr.isDetailing()) {
  3133.         su.stopPropagation(evt);
  3134.         su.preventDefault(evt);
  3135.         mgr.focusPrevious();
  3136.       } else if (mgr.isDetailing()) {
  3137.         var ev = evt || window.event;
  3138.         var target = ev.target || ev.srcElement;
  3139.         var ancestor = target.parentNode;
  3140.         if (ancestor && ancestor.className.indexOf('options-') == 0) {
  3141.           var cellIndex = ancestor.cellIndex;
  3142.           var rowIndex = ancestor.parentNode.rowIndex;
  3143.           // The header is at 0, so 'real data' starts at 1.
  3144.           if (rowIndex > 1) {
  3145.             var target = ancestor.parentNode.parentNode.rows[
  3146.                 rowIndex - 1].cells[cellIndex].firstChild;
  3147.             window.setTimeout(function() {
  3148.                   target.select();
  3149.             }, 0);
  3150.           }
  3151.         }
  3152.       }
  3153.       break;
  3154.     case su.ARROW_DOWN_KEY:
  3155.       if (!mgr.isEditing() && !mgr.isDetailing()) {
  3156.         su.stopPropagation(evt);
  3157.         su.preventDefault(evt);
  3158.         mgr.focusNext();
  3159.       } else if (mgr.isDetailing()) {
  3160.         var ev = evt || window.event;
  3161.         var target = ev.target || ev.srcElement;
  3162.         var ancestor = target.parentNode;
  3163.         if (ancestor && ancestor.className.indexOf('options-') == 0) {
  3164.           var cellIndex = ancestor.cellIndex;
  3165.           var rowIndex = ancestor.parentNode.rowIndex;
  3166.           var rows = ancestor.parentNode.parentNode.rows;
  3167.           if (rowIndex < rows.length - 1) {
  3168.             var target = rows[rowIndex + 1].cells[cellIndex].firstChild;
  3169.             window.setTimeout(function() {
  3170.                   target.select();
  3171.             }, 0);
  3172.           }
  3173.         }
  3174.       }
  3175.       break;
  3176.     case su.TAB_KEY:
  3177.  
  3178.       // If the user is inside the details panel, let tabs flow normally.
  3179.       if (mgr.isDetailing()) {
  3180.         break;
  3181.       }
  3182.  
  3183.       // If the user hits tab or enter when the edit panel is visible,
  3184.       // then apply the changes through handleEdit, but if the edit
  3185.       // panel is not visible, then tab or Enter simply remove focus
  3186.       // from the spreadsheet cell.
  3187.       su.stopPropagation(evt);
  3188.       su.preventDefault(evt);
  3189.  
  3190.       // If the edit field is currently visible we want to push any value
  3191.       // change back to SketchUp before we move the focus.
  3192.       if (mgr.isEditing()) {
  3193.         var refocus = su.getShiftKey() ? mgr.FOCUS_PREVIOUS: mgr.FOCUS_NEXT;
  3194.         mgr.tree.handleEdit($('edit-field'), mgr.getFocusedElement(), refocus,
  3195.             true);
  3196.       } else {
  3197.         // Shift-Tab moves us back, Tab moves us forward.
  3198.         if (su.getShiftKey()) {
  3199.           mgr.focusPrevious();
  3200.         } else {
  3201.           mgr.focusNext();
  3202.         }
  3203.       }
  3204.  
  3205.       break;
  3206.     case su.ENTER_KEY:
  3207.  
  3208.       // When detailing we want to watch for whether the enter key is pressed
  3209.       // when we're editing an option list. In that case we'll simulate a Tab
  3210.       // by blurring the field (to commit the value) and then add a row.
  3211.       if (mgr.isDetailing()) {
  3212.         var ev = evt || window.event;
  3213.         var target = ev.target || ev.srcElement;
  3214.         var ancestor = target.parentNode;
  3215.         if (ancestor && ancestor.className == 'options-label') {
  3216.           var label = target.value;
  3217.           try {
  3218.             target = ancestor.nextSibling.firstChild;
  3219.             if (su.isEmpty(target.value)) {
  3220.               target.value = label;
  3221.             }
  3222.             target.select();
  3223.           } catch (e) {
  3224.           }
  3225.         } else if (ancestor && ancestor.className == 'options-value') {
  3226.           mgr.tree.storeOptions(false, target);
  3227.           mgr.tree.addBlankOption();
  3228.         }
  3229.         break;
  3230.       }
  3231.  
  3232.       // If the edit field is currently visible we want to push changes to
  3233.       // SketchUp and then return focus to the current field with just the
  3234.       // highlighting rectange but no edit field.
  3235.       if (mgr.isEditing()) {
  3236.         // Hide the highlight momentarily to avoid having it out of sync with
  3237.         // any editor-related resizing.
  3238.         mgr.hideHighlight();
  3239.         mgr.tree.handleEdit($('edit-field'), mgr.getFocusedElement(),
  3240.             mgr.FOCUS_CURRENT, false);
  3241.       } else {
  3242.         if (su.isValid(mgr.getFocusedElement())) {
  3243.           window.setTimeout(function() {
  3244.             mgr.tree.editAttributeValue(mgr.getFocusedElement());
  3245.             $('edit-field').select();
  3246.             }, 0);
  3247.           return false;
  3248.         }
  3249.       }
  3250.       su.stopPropagation(evt);
  3251.       su.preventDefault(evt);
  3252.       break;
  3253.     case su.SHIFT_KEY:
  3254.       // Ignore toggling of Shift since it's often used with Tab to control
  3255.       // direction of the Tab without implying a desire to edit content.
  3256.       break;
  3257.     default:
  3258.       // All other keys should trigger editing if we're currently highlighting
  3259.       // a cell.
  3260.       if (!mgr.isEditing() && !mgr.isDetailing()) {
  3261.         if (keycode > 32) {
  3262.           if (su.isValid(mgr.getFocusedElement())) {
  3263.  
  3264.             mgr.tree.editAttributeValue(mgr.getFocusedElement());
  3265.  
  3266.             // See if we got either an = or @ as a prefix. If so then we
  3267.             // consider that an indicator that the user wants to start a new
  3268.             // formula or reference and we clear any existing value.
  3269.             // NOTE NOTE NOTE that this isn't localized by keyboard, it's
  3270.             // specific to a US Ascii 101 keyboard arrangement.
  3271.             var ev = evt || window.event;
  3272.             if ((keycode == mgr.EQUAL_KEY_STD ||
  3273.                 keycode == mgr.EQUAL_KEY_NUM) ||
  3274.                 (keycode == mgr.COMMAT_KEY && ev.shiftKey == true)) {
  3275.               $('edit-field').value = '';
  3276.             }
  3277.           }
  3278.         }
  3279.       } else {
  3280.  
  3281.         if (mgr.tree.isEditingName &&
  3282.             $('edit-field').value.length > mgr.MAX_NAME_LENGTH * 2) {
  3283.           var ev = evt || window.event;
  3284.           if ((keycode != su.BACKSPACE_KEY) && (keycode != su.DELETE_KEY)) {
  3285.             su.stopPropagation(evt);
  3286.             su.preventDefault(evt);
  3287.           }
  3288.         }
  3289.  
  3290.         // Update our editor height to match the content just entered.
  3291.         mgr.updateEditorLayoutTimeout();
  3292.         if (mgr.isFloating()) {
  3293.           mgr.updateFloatingEditorLayout();
  3294.         }
  3295.       }
  3296.       break;
  3297.   }
  3298.   return true;
  3299. };
  3300.  
  3301. /**
  3302.  * Responds to notifications that a keypress event has occurred. For fields
  3303.  * ignore these events for any of the navigation keys to ensure they don't
  3304.  * affect field content.
  3305.  * @param {Element} target The element which received the event.
  3306.  * @param {Event} evt The native keypress event.
  3307.  * @param {number} opt_manualKeycode An optional alternative keycode to force.
  3308.  * @return {boolean} True so the event default operation continues.
  3309.  */
  3310. AttributeTree.prototype.handleKeyPress = function(target, evt,
  3311.     opt_manualKeycode) {
  3312.  
  3313.   var keycode = su.ifInvalid(opt_manualKeycode, su.getKeyCode(evt));
  3314.   switch (keycode) {
  3315.     case su.ESCAPE_KEY:
  3316.     case su.ARROW_UP_KEY:
  3317.     case su.ARROW_DOWN_KEY:
  3318.     case su.TAB_KEY:
  3319.     case su.ENTER_KEY:
  3320.     case su.SHIFT_KEY:
  3321.       break;
  3322.     default:
  3323.       break;
  3324.   }
  3325.  
  3326.   return true;
  3327. };
  3328.  
  3329. /**
  3330.  * Responds to notifications that a keyup event has occurred.
  3331.  * @param {Element} target The element which received the event.
  3332.  * @param {Event} evt The native keyup event.
  3333.  * @param {number} opt_manualKeycode An optional alternative keycode to force.
  3334.  * @return {boolean} True so the event default operation continues.
  3335.  */
  3336. AttributeTree.prototype.handleKeyUp = function(target, evt,
  3337.     opt_manualKeycode) {
  3338.  
  3339.   var keycode = su.ifInvalid(opt_manualKeycode, su.getKeyCode(evt));
  3340.  
  3341.   switch (keycode) {
  3342.     case su.ESCAPE_KEY:
  3343.     case su.ARROW_UP_KEY:
  3344.     case su.ARROW_DOWN_KEY:
  3345.     case su.TAB_KEY:
  3346.     case su.ENTER_KEY:
  3347.     case su.SHIFT_KEY:
  3348.       su.preventDefault(evt);
  3349.       su.stopPropagation(evt);
  3350.       break;
  3351.     default:
  3352.       break;
  3353.   }
  3354.  
  3355.   return true;
  3356. };
  3357.  
  3358. /**
  3359.  * Hides informational rows related to configuration details.
  3360.  */
  3361. AttributeTree.prototype.hideAccessRows = function() {
  3362.   su.hide('formlabel-row');
  3363.   su.hide('units-row');
  3364.   su.hide('material-units-row');
  3365.   su.hide('options-row');
  3366. };
  3367.  
  3368. /**
  3369.  * Hides the controls typically displayed during hover which provide access
  3370.  * to editing or deleting an attribute's details.
  3371.  */
  3372. AttributeTree.prototype.hideDetailControls = function() {
  3373.   $('details-button').style.top = '' + mgr.HIDDEN_EDITOR_TOP + 'px';
  3374.   su.hide('details-button');
  3375.  
  3376.   $('delete-button').style.top = '' + mgr.HIDDEN_EDITOR_TOP + 'px';
  3377.   su.hide('delete-button');
  3378. };
  3379.  
  3380. /**
  3381.  * Hides the details editing panel.
  3382.  * @param {boolean} opt_doSave True to cause data to be saved before closing
  3383.  * the details editing panel.
  3384.  */
  3385. AttributeTree.prototype.hideDetailPanel = function(opt_doSave) {
  3386.  
  3387.   this.hideEditPanels();
  3388.   su.hide('details-panel');
  3389.   su.hide('delete-button');
  3390.  
  3391.   this.hideAccessRows();
  3392.   su.enable('cancelButton');
  3393.   su.enable('refreshButton');
  3394.   this.highlight();
  3395.  
  3396.   if (opt_doSave == true) {
  3397.  
  3398.     var entity = su.findEntity(this.idToDetail, mgr.rootEntity);
  3399.     var attrName = this.attNameToDetail;
  3400.  
  3401.     comp.setAttributeAccess(entity, attrName, $('access-pulldown').value);
  3402.     comp.setAttributeUnits(entity, attrName, $('units-pulldown').value);
  3403.  
  3404.     // If the formula units pulldown is present, then grab that value.
  3405.     if (su.isValid($('formulaunits-pulldown'))) {
  3406.       comp.setAttributeFormulaUnits(entity, attrName,
  3407.         $('formulaunits-pulldown').value);
  3408.     }
  3409.  
  3410.     var value = '' + comp.getAttributeValue(entity, attrName);
  3411.     comp.setAttributeValue(entity, attrName, su.unescapeHTML(value));
  3412.  
  3413.     // Since this is an edit action, store the refocus target as we do in
  3414.     // handleEdit().
  3415.     mgr.refocusTarget = 'value_' + this.idToDetail + '_' + attrName;
  3416.     mgr.refocusIndex = mgr.FOCUS_CURRENT;
  3417.     mgr.refocusEditor = false;
  3418.  
  3419.     if (attrName == 'scaletool') {
  3420.       comp.setAttributeValue(entity, attrName, mgr.calculateScaleTool());
  3421.     }
  3422.  
  3423.     var formLabel = $('formlabel-textbox').value;
  3424.     comp.setAttributeFormLabel(entity, attrName, formLabel);
  3425.  
  3426.     comp.pushAttribute(entity, attrName, 'mgr.redraw');
  3427.   }
  3428. };
  3429.  
  3430. /**
  3431.  * Hides the various panels which may be open as a result of an editing
  3432.  * operation.
  3433.  */
  3434. AttributeTree.prototype.hideEditPanels = function() {
  3435.  
  3436.   mgr.editPanel.style.top = '' + mgr.HIDDEN_EDITOR_TOP + 'px';
  3437.   mgr.editPanel.style.left = '0px';
  3438.   su.hide('list-panel');
  3439.  
  3440.   if (su.isValid(this.lastLabelCell)) {
  3441.     var lastClass = this.lastLabelCell.className;
  3442.     this.lastLabelCell.className = lastClass.replace(/label-selected/,
  3443.         'label');
  3444.   }
  3445.  
  3446.   mgr.setStatusBar(mgr.INTRO_STATUS);
  3447. };
  3448.  
  3449. /**
  3450.  * Responds to notification that a mouse click has occurred on an attribute.
  3451.  * The primary tasks to perform in this case are to inject attribute names
  3452.  * into formulas which are currently being edited and to update highlighting
  3453.  * as needed.
  3454.  * @param {Element} target The element which was clicked, or which
  3455.  *     should be used as if a click had occurred. This is normally provided by
  3456.  *     an onclick handler and resolves to a 'td' in those cases.
  3457.  * @param {string} entityID The entity ID for the entity whose attribute
  3458.  *     should be highlighted.
  3459.  * @param {string} attrName The name of the attribute which should be
  3460.  *     highlighted.
  3461.  */
  3462. AttributeTree.prototype.handleAttributeClick = function(target, entityID,
  3463.     attrName) {
  3464.  
  3465.   var reference;
  3466.  
  3467.   var entity = su.findEntity(entityID, mgr.rootEntity);
  3468.   var attr = comp.getAttribute(entity, attrName);
  3469.   var field = $('edit-field');
  3470.  
  3471.   var last = su.ifAbsent(this.lastAttributeSelected, 'label', null);
  3472.  
  3473.   // When we're editing a formula our job is to inject the currently selected
  3474.   // attribute's label into the formula, replacing any current selection. In
  3475.   // all other cases we simply move the highlighting to the newly selected
  3476.   // attribute's value cell.
  3477.   if (mgr.isEditing() && ((field.value.charAt(0) == '=') ||
  3478.       su.contains(mgr.FORMULA_CELL_LABELS, last))) {
  3479.  
  3480.     // Only process if we're changing either entity or attribute.
  3481.     if (entity != this.lastEntitySelected ||
  3482.         attr != this.lastAttributeSelected) {
  3483.  
  3484.       // If entity remains the same we don't want to qualify with a leading
  3485.       // entityID! prefix.
  3486.       if (entity == this.lastEntitySelected) {
  3487.         reference = comp.getAttributeLabel(entity, attrName);
  3488.       } else {
  3489.         reference = entity.name + '!' +
  3490.             comp.getAttributeLabel(entity, attrName);
  3491.       }
  3492.  
  3493.       // mgr.selectionTextRange was set in mgr.storeSelectionTextRange,
  3494.       // which is called in onmousedown so we can capture what the user
  3495.       // had selected BEFORE any other events for click/dblclick.
  3496.       su.replaceSelection(field, reference, mgr.selectionTextRange);
  3497.  
  3498.       // Adjust the editor cell so it displays properly.
  3499.       mgr.updateEditorLayout();
  3500.       mgr.floatEditorIfNecessary();
  3501.     }
  3502.   } else if (mgr.getFocusedElement() == target) {
  3503.     this.editAttributeValue(target, entityID, attrName);
  3504.   } else {
  3505.     this.hideEditPanels();
  3506.     this.highlight(target, entityID, attrName);
  3507.   }
  3508. };
  3509.  
  3510. /**
  3511.  * Highlights an attribute value cell to help the user keep track of the
  3512.  * current focal point for editing.
  3513.  * @param {Element} opt_target The element which was clicked, or which
  3514.  *     should be used as if a click had occurred. This is normally provided by
  3515.  *     an onclick handler and resolves to a 'td' in those cases.
  3516.  * @param {string} opt_entityID The entity ID for the entity whose attribute
  3517.  *     should be highlighted.
  3518.  * @param {string} opt_attrName The name of the attribute which should be
  3519.  *     highlighted.
  3520.  */
  3521. AttributeTree.prototype.highlight = function(opt_target, opt_entityID,
  3522.     opt_attrName) {
  3523.  
  3524.   // Reset the value of any previously selected element.
  3525.   mgr.resetValueCell();
  3526.  
  3527.   var target = opt_target || mgr.getFocusedElement();
  3528.   if (su.notValid(target)) {
  3529.     return;
  3530.   }
  3531.  
  3532.   if (su.notValid(opt_entityID)) {
  3533.     var entity = this.lastEntitySelected || mgr.rootEntity;
  3534.   } else {
  3535.     var entity = su.findEntity(opt_entityID, mgr.rootEntity);
  3536.   }
  3537.  
  3538.   if (su.notValid(entity)) {
  3539.     return;
  3540.   }
  3541.  
  3542.   if (su.notValid(opt_attrName)) {
  3543.     var attr = this.lastAttributeSelected;
  3544.     if (su.notValid(attr)) {
  3545.       var id = target.getAttribute('id');
  3546.       if (su.isEmpty(id)) {
  3547.         return;
  3548.       }
  3549.       var parts = mgr.parseIdIntoParts(id);
  3550.       if (parts.length < 3) {
  3551.         return;
  3552.       }
  3553.       var attrName = parts[2];
  3554.       var attr = comp.getAttribute(entity, attrName);
  3555.     }
  3556.   } else {
  3557.     var attr = comp.getAttribute(entity, opt_attrName);
  3558.   }
  3559.  
  3560.   if (su.notValid(attr)) {
  3561.     return;
  3562.   }
  3563.  
  3564.   // Clear any previous label selection.
  3565.   if (su.isValid(this.lastLabelCell)) {
  3566.     var lastClass = this.lastLabelCell.className;
  3567.     this.lastLabelCell.className = lastClass.replace(/label-selected/, 'label');
  3568.   }
  3569.  
  3570.   // Target may be either a value or label cell, we want to navigate to the
  3571.   // value cell, which will be the second TD inside the TR which holds both
  3572.   // the label and value.
  3573.   var ancestor = target.parentNode;
  3574.   if (su.notValid(ancestor)) {
  3575.     return;
  3576.   }
  3577.  
  3578.   var cell = ancestor.getElementsByTagName('TD')[1];
  3579.   if (su.notValid(cell)) {
  3580.     return;
  3581.   }
  3582.  
  3583.   // Update the label reference and highlighting to match the current cell.
  3584.   this.lastLabelCell = ancestor.getElementsByTagName('TD')[0];
  3585.   var lastClass = this.lastLabelCell.className;
  3586.   this.lastLabelCell.className = lastClass.replace(/label/, 'label-selected');
  3587.  
  3588.   // Check to make sure the element isn't a descendant of a collapsed tree.
  3589.   while (ancestor && ancestor.nodeType == Node.ELEMENT_NODE) {
  3590.     if (ancestor.className && ancestor.className.indexOf('collapsed') != -1) {
  3591.       return;
  3592.     }
  3593.     ancestor = ancestor.parentNode;
  3594.   }
  3595.  
  3596.   mgr.updateHighlightLayout(true);
  3597.  
  3598.   // Some older components may not have a label, so we can get it from the ID
  3599.   // of the cell we're on and then assign for future storage.
  3600.   if (!attr.label) {
  3601.     attr.label = cell.getAttribute('id').split('_')[2];
  3602.   }
  3603.   var label = attr.label;
  3604.  
  3605.   var statusStr = '<b>' + su.truncate(label, 40) + '</b> · ';
  3606.   var reserved = comp.RESERVED[label.toLowerCase()];
  3607.   if (su.isValid(reserved)) {
  3608.     statusStr += su.translateString(reserved.summary);
  3609.     mgr.setStatusBar(statusStr, label);
  3610.   } else {
  3611.     statusStr += su.translateString('Custom attribute.');
  3612.     mgr.setStatusBar(statusStr, 'custom');
  3613.   }
  3614.  
  3615.   // Highlighting is as relevant to current focus as editing is, so once
  3616.   // we've adjusted everything from a display perspective update our context.
  3617.   mgr.setFocusedElement(target);
  3618.   this.lastEntitySelected = entity;
  3619.   this.lastAttributeSelected = attr;
  3620.   this.isEditingName = false;
  3621.  
  3622.   // Place the detail controls to match the overall row.
  3623.   parts = mgr.parseIdIntoParts(target.getAttribute('id'));
  3624.   this.showDetailControls(target.parentNode, parts[1], parts[2]);
  3625. };
  3626.  
  3627. /**
  3628.  * Refocuses on the edit field and selects all of its text. Used specifically
  3629.  * when the user types an incorrect name and we ask them to try again.
  3630.  */
  3631. AttributeTree.prototype.refocusAfterNamingError = function() {
  3632.  
  3633.   // If the user pressed Tab to enter/complete the naming/renaming process
  3634.   // we have to stop it from moving to the value cell. If not then we're ok
  3635.   // since the editor is already in the right position.
  3636.   su.stopPropagation();
  3637.   su.preventDefault();
  3638.  
  3639.   window.setTimeout(function() {
  3640.     mgr.tree.isEditingName = true;
  3641.     mgr.tree.showEditPanel(mgr.getFocusedElement());
  3642.     $('edit-field').value = $('edit-field').value.replace(/[\r\n]/gi, '');
  3643.     $('edit-field').focus();
  3644.     $('edit-field').select();
  3645.     // If the lastAttributeSelected is null it means this is an Add
  3646.     // Attribute operation so we want to redisplay the list panel.
  3647.     if (mgr.tree.lastAttributeSelected == null) {
  3648.       su.show('list-panel');
  3649.     }
  3650.   }, 10);
  3651. };
  3652.  
  3653. /**
  3654.  * Renders the tree, placing the output in the content element defined in
  3655.  * the manager.html file.
  3656.  */
  3657. AttributeTree.prototype.render = function() {
  3658.  
  3659.   var arr = [];
  3660.  
  3661.   arr.push('<div id="', mgr.rootEntity.id, '"',
  3662.       ' class="tree-branch-root">',
  3663.       this.dumpAttributeTable(mgr.rootEntity),
  3664.       '</div>',
  3665.       '<br/><br/>');
  3666.  
  3667.   su.setContent('content', arr.join(''));
  3668.  
  3669.   // Any time we rebuild the tree's UI elements we want to refocus so any
  3670.   // previous editor or highlighting state is restored.
  3671.   mgr.refocus();
  3672.  
  3673.   mgr.updateEditorLayout();
  3674. };
  3675.  
  3676. /**
  3677.  * Sets the value of the edit field to the value provided and processes it as
  3678.  * an active edit.
  3679.  * @param {Object} value The value to set for the edit field.
  3680.  */
  3681. AttributeTree.prototype.setLabelField = function(value) {
  3682.   $('edit-field').value = value;
  3683.   this.handleEdit($('edit-field'));
  3684. };
  3685.  
  3686. /**
  3687.  * Displays the proper set of access rows based on accessLevel provided.
  3688.  * @param {string} accessLevel The access level, which should be "LIST",
  3689.  *     "NONE", or any other non-empty value.
  3690.  */
  3691. AttributeTree.prototype.showAccessRows = function(accessLevel) {
  3692.   // Start by hiding all current rows, clearing the slate.
  3693.   this.hideAccessRows();
  3694.  
  3695.   // To avoid shifting focus just because of a click on the detail button we
  3696.   // acquire the attribute as needed here.
  3697.   var entity = su.findEntity(this.idToDetail, mgr.rootEntity);
  3698.   var attrName = this.attNameToDetail;
  3699.   var attr = this.lastAttributeSelected || comp.getAttribute(entity, attrName);
  3700.  
  3701.   // When altering material via text the only choice is Arbitrary text, but
  3702.   // that's been tucked away in a row of its own.
  3703.   var unitID = 'units-row';
  3704.   if (su.isValid(attr)) {
  3705.     var name = attr.label || attr.id;
  3706.     if (su.notEmpty(name) && name.toLowerCase() == 'material') {
  3707.       unitID = 'material-units-row';
  3708.     }
  3709.   }
  3710.  
  3711.   // All levels will show the formlabel row, unless we're looking at a level
  3712.   // which turns them all off.
  3713.   if (su.notEmpty(accessLevel) && accessLevel != 'NONE') {
  3714.     su.show('formlabel-row');
  3715.     su.show(unitID);
  3716.   }
  3717.  
  3718.   // The remaining row(s) are displayed based on whether this is list mode.
  3719.   if (accessLevel == 'LIST') {
  3720.     su.show('options-row');
  3721.     su.hide('material-units-row');
  3722.     su.hide('units-row');
  3723.   }
  3724.   mgr.updateDetailPanelLayout();
  3725. };
  3726.  
  3727. /**
  3728.  * Displays the detail controls, the buttons which optionally display for an
  3729.  * attribute during highlighting.
  3730.  * @param {Element} target The target element under focus/highlight.
  3731.  * @param {string} entityID The ID of the entity containing the data.
  3732.  * @param {string} attrName The name of the specific attribute being detailed.
  3733.  */
  3734. AttributeTree.prototype.showDetailControls = function(target,
  3735.                                                     entityID,
  3736.                                                     attrName) {
  3737.   // Don't display controls when we're editing or detailing already.
  3738.   if (mgr.isEditing() || mgr.isDetailing() || mgr.isCalling()) {
  3739.     return;
  3740.   }
  3741.  
  3742.   this.idToDetail = entityID;
  3743.   this.attNameToDetail = attrName;
  3744.  
  3745.   var offset = mgr.HIGHLIGHT_EDIT_OFFSET - mgr.scrollPanel.scrollTop;
  3746.  
  3747.   // Show the Open Details Panel button.
  3748.   var elem = $('details-button');
  3749.   su.show(elem);
  3750.   elem.style.top = su.elementY(target) - offset + 2;
  3751.   elem.style.left = su.elementX(target) + su.elementWidth(target) -
  3752.       su.elementWidth('details-button');
  3753.  
  3754.   // Show the Delete This Attribute button.
  3755.   elem = $('delete-button');
  3756.   su.show(elem);
  3757.   elem.style.top = su.elementY(target) - offset;
  3758.   elem.style.left = su.elementX(target) - su.elementWidth('delete-button');
  3759. };
  3760.  
  3761. /**
  3762.  * Show the detail editing panel for the currently selected entity/attribute.
  3763.  */
  3764. AttributeTree.prototype.showDetailPanel = function() {
  3765.   var label;
  3766.   var unitGroup;
  3767.   var selected;
  3768.  
  3769.   var entity = su.findEntity(this.idToDetail, mgr.rootEntity);
  3770.   var attrName = this.attNameToDetail;
  3771.  
  3772.   var value = comp.getAttributeValue(entity, attrName);
  3773.   var formula = comp.getAttributeFormula(entity, attrName);
  3774.   var access = comp.getAttributeAccess(entity, attrName);
  3775.  
  3776.   // Which options appear for sharing an attribute depends on the attribute
  3777.   // itself. Metadata attributes are always visible. Attributes with live
  3778.   // values cannot be edited with a text box. And "behavior" attributes are
  3779.   // never visible (unless it's materials, in which case it is.)
  3780.   var isMetaDataAttribute = false;
  3781.   var hasLiveValue = false;
  3782.   var attr = comp.RESERVED[attrName];
  3783.  
  3784.   var arr = [];
  3785.   arr.push('<div id="details-sub-panel" class="details-sub-panel">',
  3786.       '<form name="details" onsubmit="return false">',
  3787.       '<table class="details-table" cellspacing="0" ',
  3788.       'onkeyup="mgr.updateLayout()">',
  3789.       '<tr><td valign="top">');
  3790.  
  3791.   arr.push('<table border="0" cellpadding="3" width="100%" cellspacing="0" ',
  3792.       'class="default-cursor">');
  3793.  
  3794.   // Display our formula units pulldown.
  3795.   var pulldownHTML = [];
  3796.   var optionCount = 0;
  3797.   var attUnitGroup = '';
  3798.   var selectedUnit = comp.getAttributeFormulaUnits(entity, attrName, false);
  3799.   var selectedString;
  3800.   var unit;
  3801.   if (su.isValid(attr)) {
  3802.     attUnitGroup = attr.unitGroup;
  3803.   }
  3804.   pulldownHTML.push('<tr><td align="right" width="1%">',
  3805.       '<nobr>', su.translateString('Units:'), '</nobr>',
  3806.       '</td>',
  3807.       '<td>');
  3808.  
  3809.   pulldownHTML.push('<select name="formulaunits-pulldown" ',
  3810.       'id="formulaunits-pulldown"',
  3811.       ' style="width: 100%" ',
  3812.       ' onfocus="this.setAttribute(\'undo-value\', this.selectedIndex)"',
  3813.       ' onchange="mgr.handleFormulaUnitsChange(\'', entity.id, '\',\'',
  3814.       attrName, '\')">');
  3815.  
  3816.   // The first option in the formula units pulldown is for the "default" unit.
  3817.   var firstOptionLabel = su.translateString('Default:') + ' ';
  3818.   if (attUnitGroup == 'LENGTH' && su.isValid(comp.RESERVED[attrName])) {
  3819.     firstOptionLabel +=
  3820.         su.translateString(conv.unitsHash[comp.lengthUnits(entity)].label);
  3821.   } else {
  3822.     firstOptionLabel += su.translateString(conv.unitsHash['STRING'].label);
  3823.   }
  3824.  
  3825.   pulldownHTML.push('<option style="color:gray" value="DEFAULT">',
  3826.       firstOptionLabel, '</option>');
  3827.  
  3828.   for (var i = 0; i < conv.units.length; i++) {
  3829.     unit = conv.units[i];
  3830.     selectedString = '';
  3831.     if (unit.name == selectedUnit) {
  3832.       selectedString = 'selected="selected"';
  3833.     }
  3834.     if ((attUnitGroup == unit.group || attUnitGroup == '') &&
  3835.         unit.configOnly != true) {
  3836.       optionCount++;
  3837.       pulldownHTML.push('<option value="', unit.name, '" ', selectedString,
  3838.           '>', su.translateString(unit.label), '</option>');
  3839.     }
  3840.   }
  3841.  
  3842.   // Close the formula units select list and associated cell/row.
  3843.   pulldownHTML.push('</select></td></tr>');
  3844.  
  3845.   // If there was more than one unit that can be validly selected for this
  3846.   // attribute, then show the pulldown. Otherwise don't bother.
  3847.   if (optionCount > 1) {
  3848.     arr = arr.concat(pulldownHTML);
  3849.   }
  3850.  
  3851.   arr.push('<tr><td align="right" width="1%">',
  3852.       '<nobr>   ', su.translateString('Display rule:'),
  3853.       '</nobr>', '</td>', '<td>');
  3854.  
  3855.   arr.push('<select name="access-pulldown" id="access-pulldown" ',
  3856.       ' style="width: 100%"',
  3857.       ' onchange="', this.id, '.showAccessRows(this.value)">');
  3858.  
  3859.   if (su.isValid(attr)) {
  3860.  
  3861.     mgr.setStatusBar('<b>' + entity.name + '!' + attr.label +
  3862.       '</b> · ' + su.translateString(attr.summary), attrName);
  3863.  
  3864.     hasLiveValue = attr.hasLiveValue;
  3865.  
  3866.     if (attr.group == comp.METADATA_GROUP) {
  3867.       arr.push('<option value="">', su.translateString(mgr.ACCESS[1].label),
  3868.           '</option>');
  3869.       isMetaDataAttribute = true;
  3870.     } else if ((attr.group == comp.BEHAVIORS_GROUP &&
  3871.         attrName != 'material') || attr.group == comp.FORM_DESIGN_GROUP) {
  3872.       arr.push('<option value="">', su.translateString(mgr.ACCESS[0].label),
  3873.           '</option>');
  3874.       isMetaDataAttribute = true;
  3875.     }
  3876.   } else {
  3877.     mgr.setStatusBar('<b>' + entity.name + '!' +
  3878.         comp.getAttributeLabel(entity, attrName) +
  3879.         '</b> · ' + su.translateString('Custom Attribute'), attrName);
  3880.   }
  3881.  
  3882.   if (isMetaDataAttribute == false) {
  3883.     for (var accessID = 0; accessID < mgr.ACCESS.length; accessID++) {
  3884.       label = mgr.ACCESS[accessID].label;
  3885.       value = mgr.ACCESS[accessID].value;
  3886.       if (value == access) {
  3887.         selected = ' selected="selected" ';
  3888.       } else {
  3889.         selected = '';
  3890.       }
  3891.       arr.push('<option value="', value, '"', selected, '>',
  3892.           su.translateString(label), '</option>');
  3893.     }
  3894.   }
  3895.   // Close the access select list and associated cell/row.
  3896.   arr.push('</select></td></tr>');
  3897.  
  3898.   // Scale tool setup.
  3899.   if (attrName == 'scaletool') {
  3900.     arr.push('<tr>',
  3901.         '<td id="scale-tool-cell" valign="top" align="right">',
  3902.         mgr.dumpScaleToolGraphic(value), '</td>',
  3903.         '<td>',
  3904.  
  3905.         '<input type="checkbox" id="scaletool_1"',
  3906.         ' onclick="mgr.calculateScaleTool()"',
  3907.         mgr.ifBitIsZero(value, 1, 'checked="checked"'), '> ',
  3908.         su.translateString('Scale along red. (X)'), '<br/>',
  3909.  
  3910.         '<input type="checkbox" id="scaletool_2" ',
  3911.         ' onclick="mgr.calculateScaleTool()"',
  3912.         mgr.ifBitIsZero(value, 2, 'checked="checked"'), '> ',
  3913.         su.translateString('Scale along green. (Y)'), '<br/>',
  3914.  
  3915.         '<input type="checkbox" id="scaletool_3" ',
  3916.         ' onclick="mgr.calculateScaleTool()"',
  3917.         mgr.ifBitIsZero(value, 3, 'checked="checked"'), '> ',
  3918.         su.translateString('Scale along blue. (Z)'), '<br/>',
  3919.  
  3920.         '<input type="checkbox" id="scaletool_4" ',
  3921.         ' onclick="mgr.calculateScaleTool()"',
  3922.         mgr.ifBitIsZero(value, 4, 'checked="checked"'), '> ',
  3923.         su.translateString('Scale in red/blue plane. (X+Z)'), '<br/>',
  3924.  
  3925.         '<input type="checkbox" id="scaletool_5" ',
  3926.         ' onclick="mgr.calculateScaleTool()"',
  3927.         mgr.ifBitIsZero(value, 5, 'checked="checked"'), '> ',
  3928.         su.translateString('Scale in green/blue plane. (Y+Z)'), '<br/>',
  3929.  
  3930.         '<input type="checkbox" id="scaletool_6" ',
  3931.         ' onclick="mgr.calculateScaleTool()"',
  3932.         mgr.ifBitIsZero(value, 6, 'checked="checked"'), '> ',
  3933.         su.translateString('Scale in red/green plane. (X+Y)'), '<br/>',
  3934.  
  3935.         '<input type="checkbox" id="scaletool_7" ',
  3936.         ' onclick="mgr.calculateScaleTool()"',
  3937.         mgr.ifBitIsZero(value, 7, 'checked="checked"'), '>',
  3938.         su.translateString('Scale uniform (from corners). (XYZ)'),
  3939.  
  3940.         '</td></tr>');
  3941.   }
  3942.  
  3943.   // Output the form label editing row.
  3944.   var formLabel = su.ifEmpty(comp.getAttributeFormLabel(entity, attrName),
  3945.       comp.getAttributeLabel(entity, attrName));
  3946.   formLabel = su.unescapeHTML(formLabel);
  3947.  
  3948.   var formLabelPrompt = 'Display label:';
  3949.  
  3950.   // The onclick attribute has some special cosmetic behavior around the
  3951.   // label, as that is its tooltip.
  3952.   if (attrName == 'onclick') {
  3953.     formLabelPrompt = 'Tool tip:';
  3954.     if (formLabel == comp.getAttributeLabel(entity, attrName)) {
  3955.       formLabel = su.translateString('Click to activate.');
  3956.     }
  3957.   }
  3958.  
  3959.   arr.push('<tr id="formlabel-row">',
  3960.       '<td align="right"><nobr>', su.translateString(formLabelPrompt),
  3961.       '</nobr></td>',
  3962.       '<td>',
  3963.       '<input name="formlabel-textbox" id="formlabel-textbox" type="textbox"',
  3964.       ' value="', formLabel, '" style="width: 100%"/>',
  3965.       '</td></tr>');
  3966.  
  3967.   // Output the unit selection row.
  3968.   arr.push('<tr id="units-row">',
  3969.       '<td align="right"><nobr>', su.translateString('Display in:'),
  3970.       '</nobr></td>');
  3971.  
  3972.   var attributeUnitGroup;
  3973.   if (su.isValid(attr)) {
  3974.     attributeUnitGroup = su.ifEmpty(attr.unitGroup, attributeUnitGroup);
  3975.   }
  3976.  
  3977.   arr.push('<td>',
  3978.       '<select name="units-pulldown" id="units-pulldown"',
  3979.       ' style="width: 100%">');
  3980.  
  3981.   var units = su.ifInvalid(comp.getAttributeUnits(entity, attrName), 'STRING');
  3982.   for (var unitID = 0; unitID < conv.units.length; unitID++) {
  3983.     label = conv.units[unitID].label;
  3984.     value = conv.units[unitID].name;
  3985.  
  3986.     unitGroup = conv.units[unitID].group;
  3987.  
  3988.     if (su.notValid(attributeUnitGroup) ||
  3989.         attributeUnitGroup == unitGroup) {
  3990.       if (value == units) {
  3991.         selected = ' selected="selected" ';
  3992.       } else {
  3993.         selected = '';
  3994.       }
  3995.       arr.push('<option value="', value, '"', selected, '>',
  3996.           su.translateString(label), '</option>');
  3997.     }
  3998.   }
  3999.   arr.push('</select></td></tr>');
  4000.  
  4001.   // Build a second row for material, which has its own unit requirements.
  4002.   arr.push('<tr id="material-units-row">',
  4003.       '<td align="right"><nobr>', su.translateString('Display in:'),
  4004.       '</nobr></td>');
  4005.   arr.push('<td>',
  4006.       '<select name="material-units-pulldown" id="material-units-pulldown"',
  4007.       ' style="width: 100%">');
  4008.   for (var unitID = 0; unitID < conv.units.length; unitID++) {
  4009.     value = conv.units[unitID].name;
  4010.     if (value != 'STRING') {
  4011.       continue;
  4012.     }
  4013.     label = conv.units[unitID].label;
  4014.     selected = ' selected="selected" ';
  4015.     arr.push('<option value="', value, '"', selected, '>',
  4016.         su.translateString(label), '</option>');
  4017.   }
  4018.   arr.push('</select></td></tr>');
  4019.  
  4020.   arr.push('<tr id="options-row">',
  4021.       '<td align="right" valign="top"> </td>',
  4022.       '<td id="options-panel">');
  4023.  
  4024.   // Since the scaletool panel has more content, hide the options table.
  4025.   if (attrName != 'scaletool') {
  4026.     arr.push(this.dumpOptionsTable(entity, attrName));
  4027.   }
  4028.  
  4029.   // Close the options-row.
  4030.   arr.push('</td></tr>');
  4031.  
  4032.   // Close the overall options-table and the row it resides in.
  4033.   arr.push('</table></td></tr>');
  4034.  
  4035.   // Close off the outer table and options div.
  4036.   arr.push('</table></div>');
  4037.  
  4038.   // Create a separate div/table for the footer button arrangement.
  4039.   arr.push('<div class="details-footer"><form>',
  4040.     '<table class="details-footer-table" cellspacing="0">',
  4041.     '<tr><td valign="middle">');
  4042.   arr.push('<tr><td valign="bottom" align="center">',
  4043.       '<input type="button" value="', su.translateString('Apply'), '"',
  4044.       ' onclick="', this.id, '.hideDetailPanel(true)">',
  4045.       '<input type="button" value="', su.translateString('Cancel'), '"',
  4046.       ' onclick="', this.id + '.hideDetailPanel(false)">',
  4047.       '</td></tr>');
  4048.   arr.push('</table></form></div>');
  4049.  
  4050.   var html = arr.join('');
  4051.   su.setContent('details-panel', html);
  4052.  
  4053.   this.showAccessRows(access);
  4054.  
  4055.   // The onclick attribute shows its label, as that is what is shown as the
  4056.   // tooltip.
  4057.   if (attrName == 'onclick') {
  4058.     su.show('formlabel-row');
  4059.   }
  4060.  
  4061.   su.show('details-panel');
  4062.   mgr.updateDetailPanelLayout();
  4063. };
  4064.  
  4065. /**
  4066.  * Displays the editing cell, a special textarea styled to overlay the
  4067.  * underlying content being edited.
  4068.  * @param {Element} target The native element the editor should position and
  4069.  *     offer editing services for.
  4070.  */
  4071. AttributeTree.prototype.showEditPanel = function(target) {
  4072.  
  4073.   mgr.setEditorTarget(target);
  4074.  
  4075.   var tab = $('edit-field-reference-tab');
  4076.  
  4077.   if (su.isValid(this.lastAttributeSelected)) {
  4078.     tab.innerHTML = this.lastEntitySelected.name + '!' +
  4079.         this.lastAttributeSelected.label;
  4080.   } else {
  4081.     tab.innerHTML = ' '
  4082.   }
  4083.  
  4084.   // Adjust the tab's top to offset it as needed. Note that we show it (but
  4085.   // that doesn't flush to the screen) so we're sure the computation is
  4086.   // correct and not affected by display: none.
  4087.   su.show(tab);
  4088.   var tabHeight = su.elementHeight(tab);
  4089.   su.hide(tab);
  4090.   tab.style.top = 0;
  4091.  
  4092.   var topY = su.elementY(target);
  4093.  
  4094.   // Account for border thickness on the PC edit field.
  4095.   if (su.IS_MAC == false) {
  4096.     topY -= 1;
  4097.   }
  4098.  
  4099.   var panel = mgr.editPanel;
  4100.   panel.style.top = topY - mgr.HIGHLIGHT_EDIT_OFFSET +
  4101.       mgr.scrollPanel.scrollTop;
  4102.   panel.setAttribute('fixedtop', '');
  4103.  
  4104.   panel.style.width = su.elementWidth(target);
  4105.   panel.style.height = su.elementHeight(target);
  4106.   panel.style.left = su.elementX(target);
  4107.  
  4108.   mgr.updateEditorLayoutTimeout();
  4109.  
  4110.   su.show(panel);
  4111. };
  4112.  
  4113. /**
  4114.  * Displays the attribute name list for selection during attribute addition.
  4115.  * @param {Object} entity The entity object we're adding an attribute for.
  4116.  */
  4117. AttributeTree.prototype.showListPanel = function(entity) {
  4118.  
  4119.   var stat = su.translateString(
  4120.       'Select an attribute from the list, or enter your own.');
  4121.   mgr.setStatusBar(stat, 'select');
  4122.  
  4123.   var arr = [];
  4124.   arr.push('<div id="list-sub-panel" class="list-sub-panel">',
  4125.       '<div>');
  4126.  
  4127.   var lastGroup = '';
  4128.  
  4129.   for (var attribute in comp.RESERVED) {
  4130.  
  4131.     // Only show attributes at are not already attached.
  4132.     if (comp.hasAttribute(entity, attribute)) {
  4133.       continue;
  4134.     }
  4135.  
  4136.     var attr = comp.RESERVED[attribute];
  4137.  
  4138.     // Do not show hidden attributes, and do not show metaData or form
  4139.     // design attributes if we're viewing a sub node, as those attributes
  4140.     // only make sense at the top level.
  4141.     if (attr.isHidden != true &&
  4142.       (mgr.rootEntity.id == entity.id ||
  4143.       (attr.group != comp.METADATA_GROUP &&
  4144.        attr.group != comp.FORM_DESIGN_GROUP))) {
  4145.  
  4146.       // Changing groups? Then output a subheading segment.
  4147.       if (attr.group != lastGroup) {
  4148.  
  4149.         var onClickJS = this.id + '.attachGroup(\'' + attr.group + '\')';
  4150.  
  4151.         var onMouseOverJS = "mgr.setStatusBar('<b>" +
  4152.             su.translateString(attr.group) + '</b> · ' +
  4153.             su.translateString('Add all.') +
  4154.             "', 'add-all'); this.innerHTML='" +
  4155.             su.translateString(attr.group) +
  4156.             ' ' + su.translateString('(add all)') + "';";
  4157.  
  4158.         var onMouseOutJS = "mgr.setStatusBar('" +
  4159.             stat + "', 'select'); this.innerHTML='" +
  4160.             su.translateString(attr.group) + "';";
  4161.  
  4162.         arr.push('</div><div class="list-group"><div class="list-head" ',
  4163.             ' onclick="', onClickJS, '"',
  4164.             ' onmouseover="this.className=\'list-head-active\';',
  4165.             'this.parentNode.className=\'list-group-active\';',
  4166.             onMouseOverJS, '"',
  4167.             ' onmouseout="this.className=\'list-head\';',
  4168.             'this.parentNode.className=\'list-group\';',
  4169.             onMouseOutJS, '"',
  4170.             '>', su.translateString(attr.group), '</div>');
  4171.         lastGroup = attr.group;
  4172.       }
  4173.  
  4174.       onClickJS = this.id + '.setLabelField(\'' + attr.label + '\')';
  4175.       onMouseOverJS = "mgr.setStatusBar('<b>" +
  4176.           attr.label +
  4177.           '</b> · ' + su.translateString(attr.summary) + "', '" +
  4178.           attribute + "');";
  4179.  
  4180.       arr.push('<div class="list-item"',
  4181.           ' onmouseover="this.className=\'list-item-active\';',
  4182.           onMouseOverJS, '"',
  4183.           ' onmouseout="this.className=\'list-item\';"',
  4184.           ' onclick="' + onClickJS, '"',
  4185.           '>', attr.label, '</div>');
  4186.     }
  4187.   }
  4188.  
  4189.   arr.push('</div><div class="list-group"><div class="list-head" ',
  4190.       ' onclick="$(\'edit-field\').select();"',
  4191.       ' onmouseover="this.className=\'list-head-active\';',
  4192.       'this.parentNode.className=\'list-group-active\';"',
  4193.       ' onmouseout="this.className=\'list-head\';',
  4194.       'this.parentNode.className=\'list-group\';"',
  4195.       '><i>', su.translateString('Or enter a custom name...'), '</i></div>');
  4196.  
  4197.   arr.push('</div>');
  4198.  
  4199.   var html = arr.join('');
  4200.  
  4201.   su.setContent('list-panel', html);
  4202.  
  4203.   // To get the list panel to position properly, move it offscreen,
  4204.   // make it visible so IE6 can get at its sizing attributes, and then
  4205.   // update its position.
  4206.   window.setTimeout(function() {
  4207.       $('list-panel').left = -6000;
  4208.       su.show('list-panel');
  4209.       mgr.updateListPanelLayout();
  4210.     }, 0);
  4211.  
  4212.   mgr.hideHighlight();
  4213. };
  4214.  
  4215. /**
  4216.  * Stores the options selected from the options panel.
  4217.  * @param {boolean} wasEditingLabel Whether the label was/is the target of the
  4218.  *     operation.
  4219.  * @param {Element} target The target element that was editing when this
  4220.  *     method was invoked.
  4221.  */
  4222. AttributeTree.prototype.storeOptions = function(wasEditingLabel, target) {
  4223.  
  4224.   var elem;
  4225.   var attr;
  4226.  
  4227.   var entity = su.findEntity(this.idToDetail, mgr.rootEntity);
  4228.   var attrName = this.attNameToDetail;
  4229.   var i = 1;
  4230.   var options = '';
  4231.   var foundEmptyLabel = false;
  4232.  
  4233.   // Pull the units to parse from directly out of the formulaunits pulldown if
  4234.   // it exists.
  4235.   var units;
  4236.  
  4237.   // If an explicit unit is selected, then use that instead of the default one.
  4238.   // This allows us to refresh the options list in the currently selected
  4239.   // units even before the change is committed.
  4240.   if (su.isValid($('formulaunits-pulldown'))) {
  4241.     units = $('formulaunits-pulldown').value;
  4242.     if (units == 'DEFAULT') {
  4243.       if (su.isValid(comp.RESERVED[attrName])) {
  4244.         if (comp.RESERVED[attrName].unitGroup == 'LENGTH') {
  4245.           units = su.ifEmpty(comp.lengthUnits(entity), units);
  4246.         }
  4247.       } else {
  4248.         units = 'STRING';
  4249.       }
  4250.     }
  4251.   } else {
  4252.     // In some of the reserved attributes, we hide the formulaunits-pulldown
  4253.     // control because it's not something that the user can change. In such a
  4254.     // case, pull the explicit formula unit for that attribute.
  4255.     units = comp.getAttributeFormulaUnits(entity, attrName, true);
  4256.   }
  4257.  
  4258.   target.setAttribute('undo-value', target.value);
  4259.  
  4260.   while (su.isValid(elem = $('option-label-' + i))) {
  4261.     var label = elem.value;
  4262.     var value = $('option-value-' + i).value;
  4263.  
  4264.     // If we've just tabbed off of a label for the first time, preload the
  4265.     // value cell with whatever we just typed in.
  4266.     if (wasEditingLabel &&
  4267.         value == su.translateString(mgr.DEFAULT_OPTION_VALUE)) {
  4268.       $('option-value-' + i).value = label;
  4269.       $('option-value-' + i).select();
  4270.     }
  4271.  
  4272.     if (value.indexOf('=') != 0 &&
  4273.         value != su.translateString(mgr.DEFAULT_OPTION_VALUE)) {
  4274.       var enteredValue = conv.parseTo(value, units);
  4275.       var baseValue = conv.toBase(enteredValue, units);
  4276.       value = '' + baseValue;
  4277.     }
  4278.  
  4279.     if (value.indexOf('=') != 0 && wasEditingLabel == false) {
  4280.       $('option-value-' + i).value = su.unescapeHTML(
  4281.           conv.format(conv.fromBase(value, units), units,
  4282.           mgr.DEFAULT_EDIT_DECIMAL_PLACES));
  4283.     }
  4284.  
  4285.     if (su.notEmpty(label) &&
  4286.         label != su.translateString(mgr.DEFAULT_OPTION_LABEL)) {
  4287.  
  4288.       options += '&' + escape(label) + '=' + escape(value);
  4289.  
  4290.       // Take a look at our current attribute value. If it's empty, then
  4291.       // let's set it to the current option value.
  4292.       if (wasEditingLabel != true) {
  4293.         var currentAttValue = comp.getAttributeValue(entity, attrName);
  4294.         if (su.isEmpty(currentAttValue)) {
  4295.           if (value.indexOf('=') == 0) {
  4296.             comp.setAttributeFormula(entity, attrName, value.substring(1));
  4297.             comp.setAttributeValue(entity, attrName, value.substring(1));
  4298.           } else {
  4299.             comp.setAttributeValue(entity, attrName, value);
  4300.           }
  4301.         }
  4302.       }
  4303.     } else if (wasEditingLabel == true) {
  4304.       foundEmptyLabel = true;
  4305.     }
  4306.     i++;
  4307.   }
  4308.  
  4309.   // Erase empty list by storing a single character.
  4310.   options += '&';
  4311.   comp.setAttributeOptions(entity, attrName, options);
  4312.  
  4313.   if (foundEmptyLabel == true) {
  4314.     su.setContent('options-panel', this.dumpOptionsTable(entity, attrName));
  4315.   }
  4316. };
  4317.  
  4318. /**
  4319.  * Toggles the collapse state of a particular entity level. Typically invoked
  4320.  * via a click on the heading element for that tree branch.
  4321.  * @param {Element} headElement The element which represents the root of a
  4322.  *     particular tree branch.
  4323.  * @param {String} entityID The ID of the specific entity whose data is being
  4324.  *     shown or hidden.
  4325.  */
  4326. AttributeTree.prototype.toggleCollapse = function(headElement, entityID) {
  4327.  
  4328.   this.hideEditPanels();
  4329.   this.hideDetailControls();
  4330.  
  4331.   // Make sure no highlighting is in place, we'll turn it back on after any
  4332.   // collapse has processed to ensure proper location of the highlight.
  4333.   mgr.hideHighlight();
  4334.   mgr.setFocusedElement(null);
  4335.   mgr.refocusIndex = null;
  4336.   mgr.refocusTarget = null;
  4337.  
  4338.   // Toggle the box display for this branch level.
  4339.   // Update the entity so it is aware of the collapse state.
  4340.   var entity = su.findEntity(entityID, mgr.rootEntity);
  4341.   var headParent = headElement.parentNode;
  4342.   if (su.notValid(entity)) {
  4343.     su.raise(su.translateString('Could not find entity: ') + entityID);
  4344.   } else if (headParent.className == 'tree-leaf-visible') {
  4345.     headParent.className = 'tree-leaf-collapsed';
  4346.     headElement.className = 'attribute-head-collapsed';
  4347.     comp.setAttributeValue(entity, '_iscollapsed', 'true');
  4348.     comp.pushAttribute(entity, '_iscollapsed');
  4349.     su.hide('add-attribute-link-' + entityID);
  4350.   } else {
  4351.     headParent.className = 'tree-leaf-visible';
  4352.     headElement.className = 'attribute-head-visible';
  4353.     comp.setAttributeValue(entity, '_iscollapsed', 'false');
  4354.     comp.pushAttribute(entity, '_iscollapsed');
  4355.     su.show('add-attribute-link-' + entityID);
  4356.   }
  4357. };
  4358.  
  4359.