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

  1. //  Copyright: Copyright 2008 Google Inc.
  2. //  License: All Rights Reserved.
  3.  
  4. /**
  5.  * @fileoverview SketchUp-wide baseline routines for coordinating WebDialog
  6.  * JavaScript logic with the SketchUp Ruby API.
  7.  * @supported Note that the functionality in this file is intended to support
  8.  * Sketchup's current use of embedded IE6+ and/or WebKit 2.0+ browsers only.
  9.  */
  10.  
  11. //  --------------------------------------------------------------------------
  12. //  Prerequisites
  13. //  --------------------------------------------------------------------------
  14.  
  15. // Define the "SketchUp" object which holds our common utility functions
  16. // and constants and serves as our public interface to these properties.
  17.  
  18. /**
  19.  * The global sketchup utilities namespace, containing functions, properties, 
  20.  * and constants which are shared by sketchup web dialog consumers.
  21.  * @type {Object}
  22.  */
  23. var su = {};
  24.  
  25. /**
  26.  * The SketchUp "information dictionary", containing key/value pairs for data
  27.  * elements such as version number, pro vs. free, etc.
  28.  * @type {Object}
  29.  */
  30. su.info = {};
  31.  
  32. /**
  33.  * The current version number of the SketchUp Ruby/JavaScript bridge logic.
  34.  * @type {number}
  35.  */
  36. su.BRIDGE_VERSION = 1.0;
  37.  
  38. /**
  39.  * Whether we are running on the Macintosh platform or not.
  40.  * @type {boolean}
  41.  */
  42. su.IS_MAC = (navigator.appVersion.indexOf('Mac') != -1) ? true : false;
  43.  
  44. // Safari doesn't track activeElement, so patch that in for compatibility
  45. // with IE by hooking the focus event.
  46. (function() {
  47.   var func;
  48.   if (!window.attachEvent) {
  49.     document.addEventListener('focus', function(evt) {
  50.       document.activeElement = evt.target;
  51.     }, true);
  52.   }
  53. }());
  54.  
  55. /**
  56.  * Declare W3C properties for jscompiler.
  57.  */
  58. var CSSPrimitiveValue;
  59.  
  60. /**
  61.  * W3C-standard DOM object containing encodings for the various node types.
  62.  * Older versions of IE don't have the Node object or W3C DOM constants
  63.  * per http://www.w3.org/TR/DOM-Level-3-Core/ecma-script-binding.html
  64.  * @type {Object}
  65.  */
  66. window.Node = window.Node || {
  67.   ELEMENT_NODE: 1,
  68.   ATTRIBUTE_NODE: 2,
  69.   TEXT_NODE: 3,
  70.   CDATA_SECTION_NODE: 4,
  71.   ENTITY_REFERENCE_NODE: 5,
  72.   ENTITY_NODE: 6,
  73.   PROCESSING_INSTRUCTION_NODE: 7,
  74.   COMMENT_NODE: 8,
  75.   DOCUMENT_NODE: 9,
  76.   DOCUMENT_TYPE_NODE: 10,
  77.   DOCUMENT_FRAGMENT_NODE: 11,
  78.   NOTATION_NODE: 12
  79. };
  80.  
  81. /**
  82.  * Keycode for a standard Down Arrow key on US keyboards.
  83.  * @type {number}
  84.  */
  85. su.ARROW_DOWN_KEY = 40;
  86.  
  87. /**
  88.  * Keycode for a standard Up Arrow key on US keyboards.
  89.  * @type {number}
  90.  */
  91. su.ARROW_UP_KEY = 38;
  92.  
  93. /**
  94.  * Keycode for backspace key on US keyboards.
  95.  */
  96. su.BACKSPACE_KEY = 8;
  97.  
  98. /**
  99.  * Keycode for delete key on US keyboards.
  100.  */
  101. su.DELETE_KEY = 46;
  102.  
  103. /**
  104.  * Keycode for a standard Return/Enter key on US keyboards.
  105.  * @type {number}
  106.  */
  107. su.ENTER_KEY = 13;
  108.  
  109. /**
  110.  * Keycode for a standard Escape key on US keyboards.
  111.  * @type {number}
  112.  */
  113. su.ESCAPE_KEY = 27;
  114.  
  115. /**
  116.  * Keycode for a standard Shift key on US keyboards.
  117.  * @type {number}
  118.  */
  119. su.SHIFT_KEY = 16;
  120.  
  121. /**
  122.  * Keycode for a standard Tab key on US keyboards.
  123.  * @type {number}
  124.  */
  125. su.TAB_KEY = 9;
  126.  
  127. /**
  128.  * Returns the element whose ID is given. If the parameter is an element it
  129.  * is returned unchanged. This routine will also check the element's name
  130.  * attribute if no element with the given ID is provided.
  131.  * @param {string|Element} elementOrID The element or element ID to find.
  132.  * @param {Element} opt_root An optional element to root the query at.
  133.  * @return {Element?} The targeted Element.
  134.  */
  135. function $(elementOrID, opt_root) {
  136.   if (su.notValid(elementOrID)) {
  137.     su.raise(su.translateString('Invalid parameter.'));
  138.     return;
  139.   }
  140.  
  141.   if (su.isString(elementOrID)) {
  142.     var el = document.getElementById(elementOrID);
  143.     if (su.isValid(el)) {
  144.       if (su.isValid(opt_root)) {
  145.         if (su.elementHasParent(el, opt_root)) {
  146.           return el;
  147.         }
  148.       } else {
  149.         return el;
  150.       }
  151.     }
  152.     var list = document.getElementsByTagName('*');
  153.     var len = list.length;
  154.     for (var i = 0; i < len; i++) {
  155.       el = list[i];
  156.  
  157.       // Note that IE will include comment nodes in '*' queries so we filter
  158.       // for that here.
  159.       if (el.nodeType != Node.ELEMENT_NODE) {
  160.         continue;
  161.       }
  162.  
  163.       if (el.getAttribute('name') == elementOrID) {
  164.         if (su.isValid(opt_root)) {
  165.           if (su.elementHasParent(el, opt_root)) {
  166.             return el;
  167.           }
  168.         } else {
  169.           return el;
  170.         }
  171.       }
  172.     }
  173.   } else if (elementOrID.nodeType == Node.ELEMENT_NODE) {
  174.     return elementOrID;
  175.   } else {
  176.     su.raise(su.translateString('Invalid parameter.'));
  177.     return;
  178.   }
  179. }
  180.  
  181. /**
  182.  * Pull as much SketchUp information as we can get and stores it into the
  183.  * su.info dictionary.
  184.  * @param {string} opt_completeCallback Optional name of a function to call
  185.  *     once the pull_information is complete.
  186.  */
  187. su.init = function(opt_completeCallback) {
  188.   su.callRuby('pull_information', {
  189.     'onsuccess': 'su.handlePullInformationSuccess',
  190.     'oncomplete': opt_completeCallback
  191.   });
  192. };
  193.  
  194. //  --------------------------------------------------------------------------
  195. //  Friendly Wrappers
  196. //  --------------------------------------------------------------------------
  197.  
  198. // These are the "public" methods that make consuming SketchUp data as simple
  199. // as possible. su.init() must be called first, ideally in the page's onload
  200. // event, so that the other details about the SketchUp model are available.
  201.  
  202. /**
  203.  * The global sketchup namespace, containing functions, properties, and
  204.  * constants which are shared by sketchup web dialog consumers.
  205.  * @type {Object}
  206.  */
  207. var skp = {};
  208.  
  209. /**
  210.  * A container for the active model entity's data.
  211.  * @type {Object}
  212.  */
  213. skp.activeModel = {};
  214.  
  215. /**
  216.  * A container for the active selection's data.
  217.  * @type {Object}
  218.  */
  219. skp.activeModel.selection = {};
  220.  
  221. /**
  222.  * A container for the active model's definitions data.
  223.  * @type {Object}
  224.  */
  225. skp.activeModel.definitions = {};
  226.  
  227. /**
  228.  * A container for the current dialog's data.
  229.  * @type {Object}
  230.  */
  231. skp.dialog = {};
  232.  
  233. /**
  234.  * Pull as much SketchUp information as we can get and stores it into the
  235.  * su.info dictionary. This is a friendly wrapper to the su.init() function
  236.  * so that end users of this file do not need to be familiar with the su
  237.  * namespace.
  238.  */
  239. skp.init = function() {
  240.   su.init();
  241. };
  242.  
  243. /**
  244.  * @return {string} The user's Sketchup platform/os.
  245.  */
  246. skp.platform = function() {
  247.   if (su.IS_MAC) {
  248.     return 'mac';
  249.   } else {
  250.     return 'windows';
  251.   };
  252. };
  253.  
  254. /**
  255.  * @return {string} The user's Sketchup language.
  256.  */
  257. skp.language = function() {
  258.   return su.info['language'];
  259. };
  260.  
  261. /**
  262.  * @return {string} The user's Sketchup version.
  263.  */
  264. skp.version = function() {
  265.   return su.info['version'];
  266. };
  267.  
  268. /**
  269.  * @return {string} The user's default units.
  270.  */
  271. skp.units = function() {
  272.   return su.info['units'];
  273. };
  274.  
  275. /**
  276.  * @return {string} The user's decimal delimiter.
  277.  */
  278. skp.decimalDelimiter = function() {
  279.   return su.ifEmpty(su.info['decimal_delimiter'], '.');
  280. };
  281.  
  282. /**
  283.  * @return {boolean} Whether Sketchup is the pro version.
  284.  */
  285. skp.isPro = function() {
  286.   return su.info['is_pro'];
  287. };
  288.  
  289. /**
  290.  * @return {number} The version of the dc code running on SketchUp.
  291.  */
  292. skp.dcVersion = function() {
  293.   return su.info['dc_version'];
  294. };
  295.  
  296. /**
  297.  * @return {number} The version of the "bridge" code.
  298.  */
  299. skp.bridgeVersion = function() {
  300.   return su.BRIDGE_VERSION;
  301. };
  302.  
  303. /**
  304.  * Tells SketchUp to open a webdialog having certain properties.
  305.  * @param {string} name Name of the window to open, which will show in the
  306.  *     title bar of the dialog.
  307.  * @param {string} url Full url you'd like to open in the new dialog.
  308.  * @param {number} opt_w Width of the new dialog, in pixels.
  309.  * @param {number} opt_h Height of the new dialog, in pixels.
  310.  * @param {number} opt_x The X or left of the new dialog, in pixels.
  311.  * @param {number} opt_y The Y or top of the new dialog, in pixels.
  312.  */
  313. skp.openWebDialog = function(name, url, opt_w, opt_h, opt_x, opt_y) {
  314.   su.callRuby('do_show_dialog', {
  315.     'name': name,
  316.     'url': url,
  317.     'w': opt_w,
  318.     'h': opt_h,
  319.     'x': opt_x,
  320.     'y': opt_y
  321.   });
  322. };
  323.  
  324. /**
  325.  * Tells SketchUp to open a new window in the user's default web browser
  326.  * outside of SketchUp.
  327.  * @param {string} url Full url you'd like to open.
  328.  */
  329. skp.openURL = function(url) {
  330.   su.callRuby('do_open_url', {'url': url});
  331. };
  332.  
  333. /**
  334.  * Tells SketchUp to resize and reposition the current dialog that this
  335.  * javascript call is made from. (Note that the x and y parameters may be
  336.  * overridden by a user's local settings, so repositioning may not work
  337.  * in all cases.)
  338.  * @param {number} w Width of the new dialog, in pixels.
  339.  * @param {number} h Height of the new dialog, in pixels.
  340.  * @param {number} opt_x The X or left of the new dialog, in pixels.
  341.  * @param {number} opt_y The Y or top of the new dialog, in pixels.
  342.  */
  343. skp.dialog.setSize = function(w, h, opt_x, opt_y) {
  344.   su.callRuby('set_dialog_properties', {
  345.     'w': w,
  346.     'h': h,
  347.     'x': opt_x,
  348.     'y': opt_y
  349.   });
  350. };
  351.  
  352. /**
  353.  * Tells SketchUp to close the current dialog.
  354. */
  355. skp.dialog.close = function() {
  356.   su.callRuby('do_close');
  357. };
  358.  
  359. /**
  360.  * Sends an "action" down to SketchUp. These actions can be any string from
  361.  * the list of ruby api actions, available at:
  362.  *    http://download.su.com/OnlineDoc/gsu6_ruby/Docs/ruby-su.html
  363.  * Also, there are four additional actions made available specifically to this
  364.  * js API:
  365.  * "generateModelXML:" Tells the Dynamic Components plugin to create a
  366.  *     local text file report of the dynamic attributes across an entire
  367.  *     model.
  368.  * "generateSelectionXML:" Tells the Dynamic Components plugin to create a
  369.  *     local text file report of the dynamic attributes across the current
  370.  *     selection.
  371.  * "generateSelectionCSV:" and "generateModelCSV:" do the same, but
  372.  *     specify a CSV format instead of xml.
  373.  * @param {string} action The action to send.
  374.  */
  375. skp.sendAction = function(action) {
  376.   su.callRuby('do_send_action', {'action': action });
  377. };
  378.  
  379. /**
  380.  * Requests a JSON report of the dynamic attributes attached to the current
  381.  * model. This will return a JSON structure that has parsed all of the Dynamic
  382.  * Component "meta attributes" into friendly named variables. Here is an
  383.  * example object that might be returned with a single component in the model.
  384.  * Note that the formula is present as a subvariable on lenx, whereas the
  385.  * actual Sketchup-side implementation is for the formula to be stored in a
  386.  * "meta attribute" called _lenx_formula.
  387.  *
  388.  * { entities[
  389.  *   { name: "myPart",
  390.  *     typename: "ComponentInstance",
  391.  *     id: 1234,
  392.  *     file: 'myPart.skp',
  393.  *     guid: '{g1234}',
  394.  *     description: 'My Part',
  395.  *     attributeDictionaries: {
  396.  *       dynamic_attributes: {
  397.  *         lenx: { value: "10" },
  398.  *         leny: { value: "20", label: "Length Y", formula: "lenx*2" }
  399.  *       }
  400.  *     }
  401.  *   }
  402.  * ]}
  403.  *
  404.  * @param {function} onDataCallback Pointer to a function that will be
  405.  *     called once the data has been received.
  406.  * @param {boolean} opt_isDeep Whether to get all nested attributes.
  407.  *     Optional. Defaults to true.
  408.  */
  409. skp.activeModel.getDynamicAttributes = function(onDataCallback, opt_isDeep) {
  410.   var deep = su.ifEmpty(opt_isDeep, true);
  411.   su.wrapperOnSuccess_ = onDataCallback;
  412.   su.callRuby('pull_attribute_tree',
  413.     {'selection_ids': 'active_model',
  414.      'onsuccess': 'su.handleWrapperSuccess',
  415.      'deep': deep });
  416. };
  417.  
  418. /**
  419.  * Requests a JSON report of the raw SU attributes attached to the current
  420.  * model and its entities. Unlike activeModel.getDynamicAttributes, this 
  421.  * method will return the raw Sketchup "meta attribute" data, such as:
  422.  *
  423.  * { entities[
  424.  *   { name: "myPart", 
  425.  *     typename: "ComponentInstance",
  426.  *     id: 1234,
  427.  *     file: 'myPart.skp',
  428.  *     guid: '{g1234}',
  429.  *     description: 'My Part',
  430.  *     attributeDictionaries: {
  431.  *       dynamic_attributes: {
  432.  *         lenx: "10",
  433.  *         leny: "20", 
  434.  *         _leny_label: "Length Y", 
  435.  *         _leny_formula: "lenx*2" 
  436.  *       }
  437.  *     }
  438.  *   }
  439.  * ]}
  440.  *
  441.  * @param {function} onDataCallback Pointer to a function that will be
  442.  *     called once the data has been received.
  443.  * @param {boolean} opt_isDeep Whether to get all nested attributes.
  444.  *     Optional. Defaults to true.
  445.  * @param {string} opt_dictionary Specific name of a dictionary to pull.
  446.  *     Optional.
  447.  */
  448. skp.activeModel.getAttributes = function(onDataCallback, opt_isDeep, 
  449.     opt_dictionary) {
  450.   var deep = su.ifEmpty(opt_isDeep, true);
  451.   su.wrapperOnSuccess_ = onDataCallback;
  452.   var dictionary = su.ifEmpty(opt_dictionary, 'all_dictionaries')
  453.   su.callRuby('pull_attribute_tree',
  454.     {'selection_ids': 'active_model',
  455.      'onsuccess': 'su.handleWrapperSuccess',
  456.      'deep': deep,
  457.      'dictionary': dictionary });
  458. };
  459.  
  460. /**
  461.  * Requests a JSON report of the dynamic attributes attached to the current
  462.  * selection.
  463.  * @param {function} onDataCallback Pointer to a function that will be
  464.  *     called once the data has been received.
  465.  * @param {boolean} opt_isDeep Whether to get all nested attributes.
  466.  *     Optional. Defaults to true.
  467.  */
  468. skp.activeModel.selection.getDynamicAttributes = function(onDataCallback,
  469.     opt_isDeep) {
  470.   var deep = su.ifEmpty(opt_isDeep, true);
  471.   su.wrapperOnSuccess_ = onDataCallback;
  472.   su.callRuby('pull_attribute_tree', {
  473.     'selection_ids': 'selection',
  474.     'onsuccess': 'su.handleWrapperSuccess',
  475.     'deep': deep
  476.   });
  477. };
  478.  
  479. /**
  480.  * Asks SketchUp to place a component that is already loaded into the SketchUp
  481.  * model.
  482.  * @param {string} definitionID SketchUp ID of the definition we want to place.
  483.  * @param {Object} opt_attributes Name/Value pairs of any DC attributes that
  484.  *     we to attach to our new instance. If this object is present, then we
  485.  *     will also do a redraw of the DC after it's placed.
  486.  */
  487. skp.activeModel.placeComponent = function(definitionID, opt_attributes) {
  488.   if (su.isEmpty(opt_attributes)) {
  489.     opt_attributes = {};
  490.   }
  491.   opt_attributes.definition_id = definitionID;
  492.   su.callRuby('do_place_component', opt_attributes);
  493. };
  494.  
  495. /**
  496.  * Asks SketchUp to load a component from a URL into the definitions. It will
  497.  * call the onDataCallback callback with an object containing an entityID
  498.  * variable if successful, or an error if unsuccessful.
  499.  * @param {string} url URL to download a SKP file from.
  500.  * @param {function} onDataCallback Function to call when complete.
  501.  */
  502. skp.activeModel.definitions.loadFromURL = function(url, onDataCallback) {
  503.   su.wrapperOnSuccess_ = onDataCallback;
  504.   su.callRuby('do_load_from_url',
  505.     {'url': url,
  506.      'onsuccess': 'su.handleWrapperSuccess'});
  507. };
  508.  
  509.  
  510. //  --------------------------------------------------------------------------
  511. //  Logging/Debugging
  512. //  --------------------------------------------------------------------------
  513.  
  514. /**
  515.  * Logs a message to the SketchUp Ruby console on behalf of JavaScript. The
  516.  * message can be provided either as a string, or in an object containing a
  517.  * key of 'message' and the message content as the value for that key.
  518.  * @param {string|Object} message The message to log in string form, or
  519.  *     an object defining keys including a 'separator', 'prefix', 'timestamp',
  520.  *     and 'message' to be output to the log.
  521.  */
  522. su.log = function(message) {
  523.   var obj;
  524.  
  525.   if (su.isString(message)) {
  526.     obj = {
  527.       'timestamp': (new Date()).getTime(),
  528.       'prefix': 'JS',
  529.       'message': message
  530.     };
  531.   } else {
  532.     obj = message;
  533.   }
  534.  
  535.   // Protect against errors in logging creating a recursive death spiral.
  536.   try {
  537.     su.callRuby('js_log', obj);
  538.   } catch (e) {
  539.     alert(message);
  540.   }
  541. };
  542.  
  543. /**
  544.  * Outputs a message to a window, opening the window as needed. This is a
  545.  * replacement for a simple alert call that offers scrolling for large output
  546.  * strings common in debugging. Note that the message is not translated.
  547.  * @param {string} message The message to output.
  548.  * @param {string} windowName The name of the window to access or open.
  549.  */
  550. su.notify = function(message, windowName) {
  551.   var arr = [];
  552.   arr.push('<html><head>',
  553.       '<link type="text/css" rel="stylesheet"',
  554.         ' href="../css/su.css"/>',
  555.       '</head><body>',
  556.       '<div class="su-notify">',
  557.         su.escapeHTML(message),
  558.       '</div></body></html>');
  559.  
  560.   var name = su.ifEmpty(windowName, 'win' + (new Date()).getTime());
  561.   var win = window.open('', name);
  562.   if (!win) {
  563.     return;
  564.   }
  565.  
  566.   var html = arr.join('');
  567.   win.document.open();
  568.   win.document.write(html);
  569.   win.document.close();
  570. };
  571.  
  572. /**
  573.  * Raises (throws) an error while offering the potential for logging and
  574.  * call stack tracing.
  575.  * @param {string|Error} message The error message to output or an Error
  576.  *     object containing the message.
  577.  * @param {Error} opt_err A native error object captured via a catch block.
  578.  */
  579. su.raise = function(message, opt_err) {
  580.   if (su.isString(message)) {
  581.  
  582.     // If we have a message our first goal is to log it if possible. Once
  583.     // that's been accomplished we can try to produce a valid Error and throw
  584.     // that to trigger onerror hooks.
  585.     try {
  586.       su.log(message);
  587.     } catch (e) {
  588.       alert(message);
  589.     }
  590.  
  591.     if (su.isValid(opt_err)) {
  592.       throw opt_err;
  593.     } else {
  594.       throw new Error(message);
  595.     }
  596.   } else {
  597.     // Presumably message is an Error so try to throw it.
  598.     throw message;
  599.   }
  600. };
  601.  
  602. /**
  603.  * Replace the standard onerror handler with one that should log to the Ruby
  604.  * console or alert (based on configuration parameters).
  605.  * @param {string} msg The error message reported by the browser.
  606.  * @param {string} url The URL of the source file containing the error.
  607.  * @param {string} line The line number where the error occurs.
  608.  */
  609. window.onerror = function(msg, url, line) {
  610.   var msg = su.translateString('ERROR: ') + msg + ' @ ' +
  611.       url.slice(url.lastIndexOf('/')) + '[' + line + ']';
  612.  
  613.   alert(msg);
  614. };
  615.  
  616. //  --------------------------------------------------------------------------
  617. //  Object Representations
  618. //  --------------------------------------------------------------------------
  619.  
  620. /**
  621.  * Returns a string representation roughly equivalent to JSON/JavaScript
  622.  * source code format which is useful for debugging object structures.
  623.  * @param {Object} anObject The object to produce a debug string for.
  624.  * @param {Array} opt_buffer An internal parameter passed by the routine
  625.  *     itself while recursively processing anObject.
  626.  * @return {string} The object in debug string format.
  627.  */
  628. su.inspect = function(anObject, opt_buffer) {
  629.   var str;
  630.   var len;
  631.   var keys;
  632.   var i;
  633.  
  634.   var arr = su.ifUndefined(opt_buffer, []);
  635.  
  636.   if (anObject === null) {
  637.     arr.push('null');
  638.   } else if (anObject === undefined) {
  639.     arr.push('undefined');
  640.   } else if (su.isString(anObject)) {
  641.     str = su.quote(anObject);
  642.     arr.push(str);
  643.   } else if (su.isDate(anObject)) {
  644.     // Dates won't work properly if left unquoted. they won't come back as
  645.     // valid Date instances unless we use Date(anObject.getTime()) as our
  646.     // string either, but at least they won't cause syntax errors for eval.
  647.     str = su.quote(anObject);
  648.     arr.push(str);
  649.   } else if (su.isScalar(anObject)) {
  650.     if (su.canCall(anObject, 'toString')) {
  651.       arr.push(anObject.toString());
  652.     } else {
  653.       arr.push('' + anObject);
  654.     }
  655.   } else if (su.isJSArray(anObject)) {
  656.     arr.push('[');
  657.     len = anObject.length;
  658.     for (i = 0; i < len; i++) {
  659.       arr.push(su.inspect(anObject[i]));
  660.       if (i + 1 < len) {
  661.         arr.push(', ');
  662.       }
  663.     }
  664.     arr.push(']');
  665.   } else {
  666.     arr.push('{');
  667.     keys = su.getKeys(anObject);
  668.     len = keys.length;
  669.     for (i = 0; i < len; i++) {
  670.       arr.push(keys[i], ':');
  671.       arr.push(su.inspect(anObject[keys[i]]));
  672.       if (i + 1 < len) {
  673.         arr.push(', ');
  674.       }
  675.     }
  676.     arr.push('}');
  677.   }
  678.  
  679.   return arr.join('');
  680. };
  681.  
  682. /**
  683.  * Resolves an object path, a dot-separated name such as su.resolveObjectPath
  684.  * by splitting on '.' and traversing to locate the leaf object.
  685.  * @param {string} aPath The object path to attempt to resolve.
  686.  * @return {Object?} The object found via the path.
  687.  */
  688. su.resolveObjectPath = function(aPath) {
  689.   if (su.isEmpty(aPath)) {
  690.     return;
  691.   }
  692.  
  693.   var obj = self;
  694.   var parts = aPath.split('.');
  695.   var len = parts.length;
  696.   for (var i = 0; i < len; i++) {
  697.     try {
  698.       obj = obj[parts[i]];
  699.       if (su.notValid(obj)) {
  700.         break;
  701.       }
  702.     } catch (e) {
  703.       obj = null;
  704.     }
  705.   }
  706.  
  707.   return obj;
  708. };
  709.  
  710. //  --------------------------------------------------------------------------
  711. //  Object Testing
  712. //  --------------------------------------------------------------------------
  713.  
  714. /**
  715.  * Adds a value to hash if the key currently doesn't map to a valid value.
  716.  * This is a useful way to augment a request object with default values.
  717.  * @param {Object} hash The object to add missing key/value pairs to.
  718.  * @param {string} key The key to check for existence.
  719.  * @param {Object} value The value to set if the key is missing/empty.
  720.  * @return {Object?} The value of the key after the set operation.
  721.  */
  722. su.addIfAbsent = function(hash, key, value) {
  723.   if (su.notValid(hash)) {
  724.     return;
  725.   }
  726.  
  727.   if (su.notValid(hash[key])) {
  728.     hash[key] = value;
  729.   }
  730.  
  731.   return hash[key];
  732. };
  733.  
  734. /**
  735.  * Returns true if the object provided supports the named function. This is
  736.  * a reasonable way of testing whether a method can be invoked on an object.
  737.  * @param {Object} suspect The object to test.
  738.  * @param {string} funcname The name of the function to test for.
  739.  * @return {boolean} True if the object supports the named function.
  740.  */
  741. su.canCall = function(suspect, funcname) {
  742.   if (su.notValid(suspect)) {
  743.     return false;
  744.   }
  745.  
  746.   return su.isFunction(suspect[funcname]);
  747. };
  748.  
  749. /**
  750.  * Returns the fallback value if the key is not found in the target object
  751.  * provided. Note that this can occur either because the object isn't valid
  752.  * or the key isn't found or isEmpty.
  753.  * @param {Object} hash The object to test.
  754.  * @param {string} key The key to look up.
  755.  * @param {Object} fallback The value to return if the key is not found.
  756.  * @return {Object} The suspect or fallback value based on isEmpty status.
  757.  */
  758. su.ifAbsent = function(hash, key, fallback) {
  759.   if (su.isEmpty(hash)) {
  760.     return fallback;
  761.   }
  762.  
  763.   return su.ifEmpty(hash[key], fallback);
  764. };
  765.  
  766. /**
  767.  * Returns the fallback value if the first parameter isEmpty.
  768.  * @param {Object} suspect The object to test.
  769.  * @param {Object} fallback The value to return if the suspect isEmpty.
  770.  * @return {Object} The suspect or fallback value based on isEmpty status.
  771.  */
  772. su.ifEmpty = function(suspect, fallback) {
  773.   return su.isEmpty(suspect) ? fallback : suspect;
  774. };
  775.  
  776. /**
  777.  * Returns the fallback value if the first parameter is notValid.
  778.  * @param {object} suspect  The object to test.
  779.  * @param {object} fallback The value to return if the suspect is notValid.
  780.  * @return {object} The suspect or fallback value based on notValid state.
  781.  */
  782. su.ifInvalid = function(suspect, fallback) {
  783.   return su.notValid(suspect) ? fallback : suspect;
  784. };
  785.  
  786. /**
  787.  * Returns the fallback value if the first parameter == undefined.
  788.  * @param {Object} suspect The object to test.
  789.  * @param {Object} fallback The value to return if the suspect isEmpty.
  790.  * @return {Object} The suspect or fallback value based on isEmpty status.
  791.  */
  792. su.ifUndefined = function(suspect, fallback) {
  793.   return su.isDefined(suspect) ? suspect : fallback;
  794. };
  795.  
  796. /**
  797.  * Returns true if the object provided is a Date instance.
  798.  * @param {Object} suspect The object to test.
  799.  * @return {boolean} True if the object is a Date instance.
  800.  */
  801. su.isDate = function(suspect) {
  802.   // Note that this relies on the object being in the same frame/window.
  803.   return (suspect != null) && (suspect.constructor === Date);
  804. };
  805.  
  806. /**
  807.  * Returns true if the suspect object is defined (meaning not explicitly
  808.  * undefined).
  809.  * @param {Object} suspect The object to test.
  810.  * @return {boolean} True if the suspect value is defined.
  811.  */
  812. su.isDefined = function(suspect) {
  813.   return typeof suspect != 'undefined';
  814. };
  815.  
  816. /**
  817.  * Returns true if the object provided is null, undefined, or the empty
  818.  * string.
  819.  * @param {Object} suspect The object to test.
  820.  * @return {boolean} True if the object is null, undefined, or ''.
  821.  */
  822. su.isEmpty = function(suspect) {
  823.   return (suspect == null) || (suspect === '') || (suspect.length == 0);
  824. };
  825.  
  826. /**
  827.  * Returns true if the object provided is an instance of Function.
  828.  * @param {Object} suspect The object to test.
  829.  * @return {boolean} True if the object is a function.
  830.  */
  831. su.isFunction = function(suspect) {
  832.   // IE has a lot of exceptions to this rule, but this is adequate.
  833.   return typeof suspect == 'function' &&
  834.       suspect.toString().indexOf('function') == 0;
  835. };
  836.  
  837. /**
  838.  * Returns true if the suspect object is a valid Array instance. Note that
  839.  * this would be called isArray but that's been taken by historical calls
  840.  * testing to see if an object is a Java array.
  841.  * @param {object} suspect The object to test.
  842.  * @return {boolean} True if the suspect value is a JavaScript array.
  843.  */
  844. su.isJSArray = function(suspect) {
  845.   // Note that this relies on the object being in the same frame/window.
  846.   return (suspect != null) && (suspect.constructor === Array);
  847. };
  848.  
  849. /**
  850.  * Returns true if the string provided appears to contain markup. This is
  851.  * used during content management to determine how to best update and
  852.  * element's content. Note that for this function to return true the string
  853.  * must start and end with < and > after whitespace is trimmed from each
  854.  * end of the string.
  855.  * @param {string} suspect The string to test.
  856.  * @return {boolean} True if the string appears to contain markup.
  857.  */
  858. su.isMarkup = function(suspect) {
  859.   if (!su.isString(suspect)) {
  860.     return false;
  861.   }
  862.  
  863.   var str = suspect.replace(/^\s*(.+?)\s*$/, '$1');
  864.  
  865.   return /^<(.*)>$/.test(str);
  866. };
  867.  
  868. /**
  869.  * Returns true if the object provided is precisely a null. Note that this
  870.  * method will return false when the input object is undefined.
  871.  * @param {Object} suspect The object to test.
  872.  * @return {boolean} True if the object is precisely null.
  873.  */
  874. su.isNull = function(suspect) {
  875.   return suspect === null;
  876. };
  877.  
  878. /**
  879.  * Returns true if the object provided is a number instance. Note that
  880.  * unlike a simple typeof check this function returns false for values which
  881.  * are isNaN allowing su.isNumber(parse[Int|Float](someval)) to work.
  882.  * @param {Object} suspect The object to test.
  883.  * @return {boolean} True if the object is a number instance.
  884.  */
  885. su.isNumber = function(suspect) {
  886.   if (isNaN(suspect)) {
  887.     return false;
  888.   }
  889.  
  890.   return typeof suspect == 'number';
  891. };
  892.  
  893. /**
  894.  * Returns true if the object provided is a scalar object, one whose value
  895.  * is roughly atomic (as opposed to an Array or Object/Hash instance).
  896.  * @param {Object} suspect The object to test.
  897.  * @return {boolean} True if the object is a scalar type instance.
  898.  */
  899. su.isScalar = function(suspect) {
  900.   if (su.isJSArray(suspect)) {
  901.     return false;
  902.   }
  903.  
  904.   // Note that this relies on the object being in the same frame/window.
  905.   return (suspect != null) && (suspect.constructor !== Object);
  906. };
  907.  
  908. /**
  909.  * Returns true if the object provided is a string instance.
  910.  * @param {Object} suspect The object to test.
  911.  * @return {boolean} True if the object is a string instance.
  912.  */
  913. su.isString = function(suspect) {
  914.   return typeof suspect == 'string';
  915. };
  916.  
  917. /**
  918.  * Returns true if the object provided is null or undefined. This is the
  919.  * preferred method for testing values for existence rather than relying on
  920.  * if (obj) where an implicit boolean type conversion may create a bug.
  921.  * @param {Object} suspect The object to test.
  922.  * @return {boolean} True if the object is null or undefined.
  923.  */
  924. su.isValid = function(suspect) {
  925.   return suspect != null;
  926. };
  927.  
  928. /**
  929.  * Returns true if the suspect element is visible, meaning that it's display
  930.  * and visibility properties imply it should be rendered in the page flow.
  931.  * @param {string|Element} elementOrID The element or element ID to find.
  932.  * @return {boolean} True if the element is found and appears visible.
  933.  */
  934. su.isVisible = function(elementOrID) {
  935.   var el = $(elementOrID);
  936.   if (su.notValid(el)) {
  937.     return false;
  938.   }
  939.  
  940.   if (el.style.display == 'none') {
  941.     return false;
  942.   }
  943.  
  944.   return el.style.visibility != 'hidden';
  945. };
  946.  
  947. /**
  948.  * Returns true if the object provided isValid and is not ''. This is
  949.  * the preferred method for testing to ensure a viable string value exists.
  950.  * @param {Object} suspect The object to test.
  951.  * @return {boolean} True if the object is valid and not the empty string.
  952.  */
  953. su.notEmpty = function(suspect) {
  954.   return !su.isEmpty(suspect);
  955. };
  956.  
  957. /**
  958.  * Returns true if the object provided is neither null or undefined. This is
  959.  * the preferred method for testing values for non-existence rather than
  960.  * relying on if (obj) where an implicit boolean type conversion may create
  961.  * a bug.
  962.  * @param {Object} suspect The object to test.
  963.  * @return {boolean} True if the object is neither null nor undefined.
  964.  */
  965. su.notValid = function(suspect) {
  966.   return suspect == null;
  967. };
  968.  
  969. //  --------------------------------------------------------------------------
  970. //  Attribute Management
  971. //  --------------------------------------------------------------------------
  972.  
  973. /**
  974.  * Returns the SketchUp attribute object (containing value, units, etc)
  975.  * found in the attribute dictionary and attribute name provided.
  976.  * @param {Object} entity The object to search for dictionary data.
  977.  * @param {string} dictionary The name of the attribute dictionary.
  978.  * @param {string} attribute The name of the attribute to locate.
  979.  * @return {Object?} The attribute value.
  980.  */
  981. su.getAttribute = function(entity, dictionary, attribute) {
  982.   var dict = su.getDictionary(entity, dictionary);
  983.   if (su.notValid(dict)) {
  984.     return;
  985.   }
  986.  
  987.   return dict[attribute];
  988. };
  989.  
  990. /**
  991.  * Returns the SketchUp attribute dictionary with the name provided.
  992.  * @param {Object} entity The object to search for dictionary data.
  993.  * @param {string} dictionary The name of the attribute dictionary.
  994.  * @return {Object?} The named dictionary.
  995.  */
  996. su.getDictionary = function(entity, dictionary) {
  997.   if (su.notValid(entity)) {
  998.     su.raise(su.translateString(
  999.         'Invalid entity. Unable to retrieve attribute dictionary.'));
  1000.     return;
  1001.   }
  1002.  
  1003.   var dicts = entity.attributeDictionaries;
  1004.   if (su.notValid(dicts)) {
  1005.     return;
  1006.   }
  1007.  
  1008.   return dicts[dictionary];
  1009. };
  1010.  
  1011. /**
  1012.  * Returns true if the named attribute exists in the identified entity.
  1013.  * object attribute dictionary.
  1014.  * @param {Object} entity The object to search.
  1015.  * @param {string} dictionary The name of the attribute dictionary.
  1016.  * @param {string} attribute The specific attribute name to locate.
  1017.  * @return {boolean} True if the named attribute exists.
  1018.  */
  1019. su.hasAttribute = function(entity, dictionary, attribute) {
  1020.   var dict = su.getDictionary(entity, dictionary);
  1021.   if (su.notValid(dict)) {
  1022.     return false;
  1023.   }
  1024.  
  1025.   return su.isDefined(dict[attribute]);
  1026. };
  1027.  
  1028. /**
  1029.  * Removes an attribute or attribute property. When a key is provided only
  1030.  * that key is removed from the attribute. When no key is provided the
  1031.  * entire attribute is removed from the dictionary.
  1032.  * @param {Object} entity The object to search for the attribute dictionary.
  1033.  * @param {string} dictionary The name of the attribute dictionary.
  1034.  * @param {string} attribute The name of the attribute to update.
  1035.  * @param {string} key The name of the aspect to update.
  1036.  * @return {boolean} True if the operation succeeded, false otherwise.
  1037.  */
  1038. su.removeAttribute = function(entity, dictionary, attribute, key) {
  1039.   var attr = su.getAttribute(entity, dictionary, attribute);
  1040.  
  1041.   if (su.notEmpty(key)) {
  1042.     if (su.isValid(attr)) {
  1043.       delete attr[key];
  1044.       return true;
  1045.     }
  1046.   } else {
  1047.     if (su.notValid(attr)) {
  1048.       return false;
  1049.     }
  1050.  
  1051.     var dicts = entity.attributeDictionaries;
  1052.     if (su.isValid(dicts)) {
  1053.       var dict = dicts[dictionary];
  1054.       if (su.isValid(dict)) {
  1055.         delete dict[attribute];
  1056.         return true;
  1057.       }
  1058.     }
  1059.   }
  1060.  
  1061.   return false;
  1062. };
  1063.  
  1064. /**
  1065.  * Sets an aspect of an named attribute to the value provided. The aspect
  1066.  * (aka key) is typically something such as 'value', 'units', or a similar
  1067.  * named property of a SketchUp attribute.
  1068.  * @param {Object} entity The object to search for the attribute dictionary.
  1069.  * @param {string} dictionary The name of the attribute dictionary.
  1070.  * @param {string} attribute The name of the attribute to update.
  1071.  * @param {string} key The name of the aspect to update.
  1072.  * @param {Object} value The value to set for the key.
  1073.  * @return {Object} The value after the set has been processed.
  1074.  */
  1075. su.setAttribute = function(entity, dictionary, attribute, key, value) {
  1076.   if (su.notValid(entity)) {
  1077.     su.raise(su.translateString('Invalid entity. No attributes can be set.'));
  1078.     return;
  1079.   }
  1080.  
  1081.   var attr = su.getAttribute(entity, dictionary, attribute);
  1082.  
  1083.   // Create attribute (and any portions of the path to that attribute) as
  1084.   // needed when the attribute can't be found.
  1085.   if (su.notValid(attr)) {
  1086.     var dicts = entity.attributeDictionaries;
  1087.     if (su.notValid(dicts)) {
  1088.       dicts = {};
  1089.       entity.attributeDictionaries = dicts;
  1090.     }
  1091.  
  1092.     var dict = dicts[dictionary];
  1093.     if (su.notValid(dict)) {
  1094.       dict = {};
  1095.       dicts[dictionary] = dict;
  1096.     }
  1097.  
  1098.     attr = {};
  1099.     dict[attribute] = attr;
  1100.   }
  1101.  
  1102.   attr[key] = value;
  1103.  
  1104.   return attr[key];
  1105. };
  1106.  
  1107. //  --------------------------------------------------------------------------
  1108. //  Collections 
  1109. //  --------------------------------------------------------------------------
  1110.  
  1111. /**
  1112.  * Tests aCollection to see if it contains aValue and returns true when the
  1113.  * value is found.
  1114.  * @param {Object} aCollection The collection, typically an Array, to test.
  1115.  * @param {Object} aValue The value, typically a string, to search for.
  1116.  * @return {boolean} True when the value is found, false otherwise.
  1117.  */
  1118. su.contains = function(aCollection, aValue) {
  1119.  
  1120.   if (su.isJSArray(aCollection)) {
  1121.     var len = aCollection.length;
  1122.     for (var i = 0; i < len; i++) {
  1123.       if (aCollection[i] == aValue) {
  1124.         return true;
  1125.       }
  1126.     }
  1127.   }
  1128.  
  1129.   return false;
  1130. };
  1131.  
  1132. //  --------------------------------------------------------------------------
  1133. //  Content Management
  1134. //  --------------------------------------------------------------------------
  1135.  
  1136. /**
  1137.  * Returns the content of the element provided or identified by ID.
  1138.  * @param {string|Element} elementOrID The element or element ID to find.
  1139.  * @return {string} The HTML content of the element.
  1140.  */
  1141. su.getContent = function(elementOrID) {
  1142.   var el = $(elementOrID);
  1143.   if (su.isValid(el)) {
  1144.     return el.innerHTML;
  1145.   }
  1146. };
  1147.  
  1148. /**
  1149.  * Sets the content of the element provided or identified by ID. The content
  1150.  * is checked for HTML and stripped of any script tags which are found.
  1151.  * @param {string|Element} elementOrID The element or element ID to find.
  1152.  * @param {string} content The new content to use for the element.
  1153.  * @param {boolean} sanitize True to force the content to be safety-checked.
  1154.  * @return {Element} The element identified by elementOrID.
  1155.  */
  1156. su.setContent = function(elementOrID, content, sanitize) {
  1157.   var el = $(elementOrID);
  1158.   if (su.notValid(el)) {
  1159.     return;
  1160.   }
  1161.  
  1162.   var text = content || '';
  1163.  
  1164.   try {
  1165.     el.innerHTML = '' + text;
  1166.   } catch (e) {
  1167.     su.raise(su.translateString('Could not set content: ') + e.message);
  1168.   }
  1169.  
  1170.   return el;
  1171. };
  1172.  
  1173. //  --------------------------------------------------------------------------
  1174. //  DOM Operations
  1175. //  --------------------------------------------------------------------------
  1176.  
  1177. /**
  1178.  * Returns true if the element is a descendant of ancestor.
  1179.  * @param {Element} element The descendant element.
  1180.  * @param {Element} ancestor The ancestor to check for containment.
  1181.  * @return {boolean} True when ancestor contains element as a descendant.
  1182.  */
  1183. su.elementHasParent = function(element, ancestor) {
  1184.  
  1185.   var parent = element.parentNode;
  1186.   while (parent && (parent.nodeType == Node.ELEMENT_NODE)) {
  1187.     if (parent === ancestor) {
  1188.       return true;
  1189.     }
  1190.     parent = parent.parentNode;
  1191.   }
  1192.  
  1193.   return false;
  1194. };
  1195.  
  1196. //  --------------------------------------------------------------------------
  1197. //  Dynamic CSS
  1198. //  --------------------------------------------------------------------------
  1199.  
  1200. /**
  1201.  * Adds a new link element to the document provided, ensuring the new
  1202.  * element's HREF points to the css file URL provided.
  1203.  * @param {Document} doc The document receiving the new CSS link element.
  1204.  * @param {string} url The CSS style URL to add.
  1205.  * @return {Element} The newly created link element.
  1206.  */
  1207. su.addStylesheet = function(doc, url) {
  1208.  
  1209.   if (su.isEmpty(url)) {
  1210.     return;
  1211.   }
  1212.  
  1213.   if (/\.css$/.test(url) != true) {
  1214.     su.raise('Invalid CSS URL: ' + url);
  1215.     return;
  1216.   }
  1217.  
  1218.   var link = doc.createElement('link');
  1219.  
  1220.   link.setAttribute('type', 'text/css');
  1221.   link.setAttribute('rel', 'stylesheet');
  1222.   link.setAttribute('media', 'screen, projection');
  1223.   link.setAttribute('href', url);
  1224.  
  1225.   doc.getElementsByTagName('head')[0].appendChild(link);
  1226.  
  1227.   return link;
  1228. };
  1229.  
  1230. /**
  1231.  * Returns the current value for a specific style property of an element.
  1232.  * @param {string|Element} elementOrID The element or element ID to find.
  1233.  * @param {string} propertyName The style property to look up.
  1234.  * @return {string} The style property value.
  1235.  */
  1236. su.getComputedStyle = function(elementOrID, propertyName) {
  1237.  
  1238.   var styleObj;
  1239.   var el = $(elementOrID);
  1240.  
  1241.   if (su.IS_MAC) {
  1242.     styleObj = su.elementWindow(el).getComputedStyle(el, null);
  1243.   } else {
  1244.     styleObj = el.currentStyle;
  1245.   }
  1246.  
  1247.   return styleObj[propertyName];
  1248. };
  1249.  
  1250. /**
  1251.  * Removes a link element to the document provided, ensuring the sheet no
  1252.  * longer applies to the content.
  1253.  * @param {Document} doc The document whose link element is being removed.
  1254.  * @param {string} url The CSS style URL to remove.
  1255.  * @return {boolean} True if the link was found and removed.
  1256.  */
  1257. su.removeStylesheet = function(doc, url) {
  1258.   var list = doc.getElementsByTagName('link');
  1259.   var len = list.length;
  1260.   for (var i = 0; i < len; i++) {
  1261.     var link = list[i];
  1262.     if (link.getAttribute('href') == url) {
  1263.       link.setAttribute('disabled', true);
  1264.       link.parentNode.removeChild(link);
  1265.       return true;
  1266.     }
  1267.   }
  1268.  
  1269.   return false;
  1270. };
  1271.  
  1272. //  --------------------------------------------------------------------------
  1273. //  Dynamic HTML
  1274. //  --------------------------------------------------------------------------
  1275.  
  1276. /**
  1277.  * Sets the disabled state of an element to true, rendering it incapable of
  1278.  * action. This is the inverse of the su.enable function.
  1279.  * @param {string|Element} elementOrID The element or element ID to find.
  1280.  * @return {Element} The element that was disabled.
  1281.  */
  1282. su.disable = function(elementOrID) {
  1283.   var el = $(elementOrID);
  1284.   if (su.isValid(el)) {
  1285.     el.disabled = true;
  1286.   }
  1287.  
  1288.   return el;
  1289. };
  1290.  
  1291. /**
  1292.  * Returns the true border box for an element, allowing accurate computation
  1293.  * of a global X, Y coordinate as well as width/height.
  1294.  * @param {string|Element} elementOrID The element or element ID to find.
  1295.  * @return {Object} An object whose keys are 'top','right','bottom', and
  1296.  *     'left', and which contains the border box dimensions and location.
  1297.  */
  1298. su.elementGetBorderBox = function(elementOrID) {
  1299.   var element = $(elementOrID);
  1300.   var elementWin = su.elementWindow(element);
  1301.   var elementDoc = elementWin.document;
  1302.  
  1303.   if (su.IS_MAC) {
  1304.  
  1305.     var offsetX = element.offsetLeft;
  1306.     var offsetY = element.offsetTop;
  1307.  
  1308.     var lastOffset = element;
  1309.     var styleObj = elementWin.getComputedStyle(element, null);
  1310.     var addDocScroll = (styleObj.position == 'fixed');
  1311.  
  1312.     var offsetParent = element.offsetParent;
  1313.  
  1314.     // Compute in all of the offset parents.
  1315.     while (su.isValid(offsetParent)) {
  1316.  
  1317.       // Add the offsets themselves.
  1318.       offsetX += offsetParent.offsetLeft;
  1319.       offsetY += offsetParent.offsetTop;
  1320.  
  1321.       // Safari does not include the border on offset parents,
  1322.       offsetX += su.elementGetBorderInPixels_(offsetParent, 'LEFT');
  1323.       offsetY += su.elementGetBorderInPixels_(offsetParent, 'TOP');
  1324.  
  1325.       styleObj = elementWin.getComputedStyle(offsetParent, null);
  1326.  
  1327.       // If any of the offset parents have a position of 'fixed',
  1328.       // then we'll want to add in the document scroll values later.
  1329.       if (addDocScroll == false && styleObj.position == 'fixed') {
  1330.         addDocScroll = true;
  1331.       }
  1332.  
  1333.       // Keep track of the last offsetParent (unless it's the body).
  1334.       if (/^body$/i.test(offsetParent.tagName) == false) {
  1335.         lastOffset = offsetParent;
  1336.       }
  1337.  
  1338.       offsetParent = offsetParent.offsetParent;
  1339.     }
  1340.  
  1341.     var elemParent = element.parentNode;
  1342.  
  1343.     // Compute in all of the scroll values of the parentNodes (not
  1344.     // necessarily the offset parents).
  1345.     while (elemParent && elemParent.tagName &&
  1346.         /^(body|html)$/i.test(elemParent.tagName) == false) {
  1347.  
  1348.       var parentStyle = elementWin.getComputedStyle(elemParent, null);
  1349.  
  1350.       //  Subtract the scroll amounts unless the parent is 'inline'
  1351.       //  or 'table'.
  1352.       if (/^inline|table.*$/.test(parentStyle.display) == false)
  1353.       {
  1354.         offsetX -= elemParent.scrollLeft;
  1355.         offsetY -= elemParent.scrollTop;
  1356.       };
  1357.  
  1358.       elemParent = elemParent.parentNode;
  1359.     };
  1360.  
  1361.     //  If we're supposed to add in the document scroll values because
  1362.     //  this flag got flipped above, then do so now.
  1363.     if (addDocScroll == true)
  1364.     {
  1365.       offsetX += elementWin.pageXOffset;
  1366.       offsetY += elementWin.pageYOffset;
  1367.     };
  1368.  
  1369.     offsetWidth = element.offsetWidth;
  1370.     offsetHeight = element.offsetHeight;
  1371.  
  1372.   } else {
  1373.  
  1374.     var elementBox = element.getBoundingClientRect();
  1375.  
  1376.     var offsetX = elementBox.left;
  1377.     var offsetY = elementBox.top;
  1378.  
  1379.     var offsetWidth = elementBox.right - offsetX;
  1380.     var offsetHeight = elementBox.bottom - offsetY;
  1381.  
  1382.     // Don't overlook adjusting for scrolling, but note that we have to
  1383.     // determine this based on the compatibility mode of the document.
  1384.     var scrollX = (elementDoc.compatMode == 'CSS1Compat') ?
  1385.         document.documentElement.scrollLeft :
  1386.         document.body.scrollLeft;
  1387.     offsetX += scrollX;
  1388.  
  1389.     var scrollY = (elementDoc.compatMode == 'CSS1Compat') ?
  1390.         document.documentElement.scrollTop :
  1391.         document.body.scrollTop;
  1392.     offsetY += scrollY;
  1393.  
  1394.     // Note also that getClientBoundingRect returns "border box" dimensions
  1395.     // so we compensate for the document element offsets from the true box.
  1396.     offsetX -= elementDoc.documentElement.clientLeft;
  1397.     offsetY -= elementDoc.documentElement.clientTop;
  1398.   }
  1399.  
  1400.   var box = {
  1401.     'left': offsetX,
  1402.     'top': offsetY,
  1403.     'width': offsetWidth,
  1404.     'height': offsetHeight
  1405.   };
  1406.  
  1407.   return box;
  1408. };
  1409.  
  1410. /**
  1411.  * Returns the border size, in pixels, for a particular side of an element.
  1412.  * Side should be specified as 'TOP', 'RIGHT', 'BOTTOM', or 'LEFT'.
  1413.  * @param {string|Element} elementOrID The element or element ID to find.
  1414.  * @param {string} side The side, as an uppercase value.
  1415.  * @return {number} The width of the border in pixels.
  1416.  * @private
  1417.  */
  1418. su.elementGetBorderInPixels_ = function(elementOrID, side)
  1419. {
  1420.   var computedStyle;
  1421.   var valueInPixels = 0;
  1422.   var element = $(elementOrID);
  1423.  
  1424.   if (su.IS_MAC == true) {
  1425.  
  1426.     // Grab the computed style for the element.
  1427.     computedStyle = su.elementWindow(element).getComputedStyle(element,
  1428.         null);
  1429.  
  1430.     try {
  1431.       switch (side) {
  1432.         case 'TOP':
  1433.           valueInPixels = computedStyle.getPropertyCSSValue(
  1434.               'border-top-width').getFloatValue(
  1435.               CSSPrimitiveValue.CSS_PX);
  1436.           break;
  1437.  
  1438.         case 'RIGHT':
  1439.           valueInPixels = computedStyle.getPropertyCSSValue(
  1440.               'border-right-width').getFloatValue(
  1441.               CSSPrimitiveValue.CSS_PX);
  1442.           break;
  1443.  
  1444.         case 'BOTTOM':
  1445.           valueInPixels = computedStyle.getPropertyCSSValue(
  1446.               'border-bottom-width').getFloatValue(
  1447.               CSSPrimitiveValue.CSS_PX);
  1448.           break;
  1449.  
  1450.         case 'LEFT':
  1451.           valueInPixels = computedStyle.getPropertyCSSValue(
  1452.               'border-left-width').getFloatValue(
  1453.               CSSPrimitiveValue.CSS_PX);
  1454.           break;
  1455.       }
  1456.     } catch (e) {
  1457.       // Our valueInPixels is already set to 0. Nothing to do here.
  1458.     }
  1459.   } else {
  1460.  
  1461.     computedStyle = element.currentStyle;
  1462.  
  1463.     switch (side) {
  1464.       case 'TOP':
  1465.         valueInPixels = computedStyle.borderTopWidth;
  1466.         break;
  1467.       case 'RIGHT':
  1468.         valueInPixels = computedStyle.borderRightWidth;
  1469.         break;
  1470.       case 'BOTTOM':
  1471.         valueInPixels = computedStyle.borderBottomWidth;
  1472.         break;
  1473.       case 'LEFT':
  1474.         valueInPixels = computedStyle.borderLeftWidth;
  1475.         break;
  1476.     }
  1477.  
  1478.     valueInPixels = Math.max(parseFloat(valueInPixels), 0);
  1479.   }
  1480.  
  1481.   return valueInPixels;
  1482. };
  1483.  
  1484. /**
  1485.  * Returns the height in pixels of the element provided.
  1486.  * @param {string|Element} elementOrID The element or element ID to find.
  1487.  * @return {number} A height in pixels.
  1488.  */
  1489. su.elementHeight = function(elementOrID) {
  1490.   var box = su.elementGetBorderBox(elementOrID);
  1491.   return box.height;
  1492. };
  1493.  
  1494. /**
  1495.  * Returns the width in pixels of the element provided.
  1496.  * @param {string|Element} elementOrID The element or element ID to find.
  1497.  * @return {number} A width in pixels.
  1498.  */
  1499. su.elementWidth = function(elementOrID) {
  1500.   var box = su.elementGetBorderBox(elementOrID);
  1501.   return box.width;
  1502. };
  1503.  
  1504. /**
  1505.  * Returns the window for the element provided.
  1506.  * @param {string|Element} elementOrID The element or element ID to find.
  1507.  * @return {Window} Returns the element's containing window.
  1508.  */
  1509. su.elementWindow = function(elementOrID) {
  1510.   var el = $(elementOrID);
  1511.   if (su.IS_MAC) {
  1512.     return el.ownerDocument.defaultView;
  1513.   } else {
  1514.     return el.ownerDocument.parentWindow;
  1515.   }
  1516. };
  1517.  
  1518. /**
  1519.  * Returns the X coordinate, in pixels, of the top left corner of the
  1520.  * element.
  1521.  * @param {string|Element} elementOrID The element or element ID to find.
  1522.  * @return {number} An X coordinate in pixels.
  1523.  */
  1524. su.elementX = function(elementOrID) {
  1525.   var box = su.elementGetBorderBox(elementOrID);
  1526.   return box.left;
  1527. };
  1528.  
  1529. /**
  1530.  * Returns the Y coordinate, in pixels, of the top left corner of the
  1531.  * element.
  1532.  * @param {string|Element} elementOrID The element or element ID to find.
  1533.  * @return {number} A Y coordinate in pixels.
  1534.  */
  1535. su.elementY = function(elementOrID) {
  1536.   var box = su.elementGetBorderBox(elementOrID);
  1537.   return box.top;
  1538. };
  1539.  
  1540. /**
  1541.  * Sets the disabled state of an element to false, returning it to an
  1542.  * enabled state. This is the inverse of the su.disable function.
  1543.  * @param {string|Element} elementOrID The element or element ID to find.
  1544.  * @return {Element} The element that was enabled.
  1545.  */
  1546. su.enable = function(elementOrID) {
  1547.   var el = $(elementOrID);
  1548.   if (su.isValid(el)) {
  1549.     el.disabled = false;
  1550.   }
  1551.  
  1552.   return el;
  1553. };
  1554.  
  1555. /**
  1556.  * Sets the visiblity of an element to 'hidden'. This is the inverse of the
  1557.  * su.show function.
  1558.  * @param {string|Element} elementOrID The element or element ID to find.
  1559.  * @return {Element} The element that was hidden.
  1560.  */
  1561. su.hide = function(elementOrID) {
  1562.   var el = $(elementOrID);
  1563.   if (su.isValid(el)) {
  1564.     var display = su.getComputedStyle(el, 'display');
  1565.     if (display != 'none') {
  1566.       el.original_display = display;
  1567.     }
  1568.     el.style.display = 'none';
  1569.     el.style.visibility = 'hidden';
  1570.   }
  1571.  
  1572.   return el;
  1573. };
  1574.  
  1575. /**
  1576.  * Sets the visibility of an element to 'visible'. This is the inverse of
  1577.  * the su.hide function.
  1578.  * @param {string|Element} elementOrID The element or element ID to find.
  1579.  * @return {Element} The element that was shown.
  1580.  */
  1581. su.show = function(elementOrID) {
  1582.   var el = $(elementOrID);
  1583.   if (su.isValid(el)) {
  1584.     var display = su.ifEmpty(el.original_display, 'block');
  1585.     el.style.display = display;
  1586.     el.style.visibility = 'visible';
  1587.   }
  1588.  
  1589.   return el;
  1590. };
  1591.  
  1592. //  --------------------------------------------------------------------------
  1593. //  Entity Management
  1594. //  --------------------------------------------------------------------------
  1595.  
  1596. // Entities are effectively visual elements in the SketchUp model. An entity
  1597. // seen from JavaScript is just a data structure consisting of specific key
  1598. // and value pairs and specific child dictionary and array content. Entity
  1599. // data is built by the Ruby API in JSON format and sent to JavaScript via the
  1600. // JavaScript/Ruby bridge (see that section in this file for more info).
  1601. //
  1602. // Unlike the JavaScript DOM, entities don't have parent links, so searches
  1603. // for parents must descend from a root which should contain the child.
  1604. // Additional variations from JavaScript also exist, hence special functions
  1605. // are implemented to manage entities different from typical DOM actions.
  1606.  
  1607. /**
  1608.  * Returns a specific subentity dictionary from a parent entity.
  1609.  * @param {string} id The subentity ID to search for.
  1610.  * @param {Object} entity The parent entity to search.
  1611.  * @param {boolean} deep False to turn off deep searching.
  1612.  * @return {Object?} The entity with ID id.
  1613.  */
  1614. su.findEntity = function(id, entity, deep) {
  1615.   if (su.notValid(entity)) {
  1616.     su.raise(su.translateString(
  1617.         'Invalid parent entity. Cannot find subentity.'));
  1618.     return;
  1619.   }
  1620.  
  1621.   if (entity.id == id) {
  1622.     return entity;
  1623.   }
  1624.  
  1625.   // No children? No chance of finding it then.
  1626.   var subs = entity.subentities;
  1627.   if (su.notValid(subs)) {
  1628.     return;
  1629.   }
  1630.  
  1631.   // Check each subentity, optionally recursing if this is a deep search.
  1632.   var len = subs.length;
  1633.   for (var i = 0; i < len; i++) {
  1634.     var sub = subs[i];
  1635.     if (sub.id == id) {
  1636.       return sub;
  1637.     }
  1638.     if (deep != false) {
  1639.       var descendant = su.findEntity(id, sub, deep);
  1640.       if (su.isValid(descendant)) {
  1641.         return descendant;
  1642.       }
  1643.     }
  1644.   }
  1645. };
  1646.  
  1647. /**
  1648.  * Returns the direct parent entity for the entity ID provided, starting the
  1649.  * search within the entity given. If deep is not false then the search is
  1650.  * done across the entire tree of entities.
  1651.  * @param {string} id The subentity ID to search for.
  1652.  * @param {Object} entity The parent entity to search.
  1653.  * @param {boolean} deep False to turn off deep searching.
  1654.  * @return {Object?} The parent of the entity with ID id.
  1655.  */
  1656. su.findEntityParent = function(id, entity, deep) {
  1657.   if (su.notValid(entity)) {
  1658.     su.raise(su.translateString(
  1659.         'Invalid parent entity. Cannot find subentity parent.'));
  1660.     return;
  1661.   }
  1662.  
  1663.   if (entity.id == id) {
  1664.     return;
  1665.   }
  1666.  
  1667.   // No children? No chance of finding it then.
  1668.   var subs = entity.subentities;
  1669.   if (su.notValid(subs)) {
  1670.     return;
  1671.   }
  1672.  
  1673.   // Check each subentity, optionally recursing if this is a deep search.
  1674.   var len = subs.length;
  1675.   for (var i = 0; i < len; i++) {
  1676.     var sub = subs[i];
  1677.     if (sub.id == id) {
  1678.       // Note that when we find a first-level subentity the parent we
  1679.       // return is the entity that rooted this level's search.
  1680.       return entity;
  1681.     }
  1682.     if (deep != false) {
  1683.       var ancestor = su.findEntityParent(id, sub, deep);
  1684.       if (su.isValid(ancestor)) {
  1685.         return ancestor;
  1686.       }
  1687.     }
  1688.   }
  1689. };
  1690.  
  1691. //  --------------------------------------------------------------------------
  1692. //  Event Management
  1693. //  --------------------------------------------------------------------------
  1694.  
  1695. /**
  1696.  * Returns the key code from the event provided, or the current window event
  1697.  * when no event is specified.
  1698.  * @param {Event} opt_evt The native keyboard event.
  1699.  * @return {number} The key code.
  1700.  */
  1701. su.getKeyCode = function(opt_evt) {
  1702.   var ev = opt_evt || window.event;
  1703.   var code = su.ifInvalid(ev.keyCode, ev.which);
  1704.  
  1705.   return code;
  1706. };
  1707.  
  1708. /**
  1709.  * Returns the key code from the event provided, or the current window event
  1710.  * when no event is specified.
  1711.  * @param {Event} opt_evt The native keyboard event.
  1712.  * @return {number} The key code.
  1713.  */
  1714. su.getShiftKey = function(opt_evt) {
  1715.   var ev = opt_evt || window.event;
  1716.   return ev.shiftKey;
  1717. };
  1718.  
  1719. /**
  1720.  * Prevents default event handling for the event provided, or the current
  1721.  * window event when no event is specified.
  1722.  * @param {Event} opt_evt The native event.
  1723.  * @return {boolean} A true or false value appropriate to the event.
  1724.  */
  1725. su.preventDefault = function(opt_evt) {
  1726.   var ev = opt_evt || window.event;
  1727.  
  1728.   if (su.canCall(ev, 'preventDefault')) {
  1729.     ev.preventDefault();
  1730.   } else {
  1731.     if (ev.type == 'mouseout') {
  1732.       ev.returnValue = true;
  1733.       return true;
  1734.     } else {
  1735.       ev.returnValue = false;
  1736.     }
  1737.   }
  1738.  
  1739.   return false;
  1740. };
  1741.  
  1742. /**
  1743.  * Cancels event propagation (and bubbling) for the event provided, or the
  1744.  * current window event when none is provided. Note that this will only work
  1745.  * on Event instances which support cancellation.
  1746.  * @param {Event} opt_evt The native event.
  1747.  * @return {boolean} A true or false value appropriate to the event.
  1748.  */
  1749. su.stopPropagation = function(opt_evt) {
  1750.   var ev = opt_evt || window.event;
  1751.  
  1752.   if (su.canCall(ev, 'stopPropagation')) {
  1753.     ev.stopPropagation();
  1754.   } else {
  1755.     ev.cancelBubble = true;
  1756.   }
  1757.  
  1758.   return false;
  1759. };
  1760.  
  1761. //  --------------------------------------------------------------------------
  1762. //  Formatting/Translation
  1763. //  --------------------------------------------------------------------------
  1764.  
  1765. /**
  1766.  * Escapes HTML entities, making sure the returned string can be used in
  1767.  * HTML content effectively.
  1768.  * @param {string} text The string to escape.
  1769.  * @return {string} The escaped text.
  1770.  */
  1771. su.escapeHTML = function(text) {
  1772.   return text.replace(/&/g, '&'
  1773.     ).replace(/"/g, '"'
  1774.     ).replace(/'/g, '''
  1775.     ).replace(/</g, '<'
  1776.     ).replace(/>/g, '>'
  1777.     ).replace(/\\/g, '\');
  1778. };
  1779.  
  1780. /**
  1781.  * Formats an object length, providing the formatted length to JavaScript
  1782.  * via the Ruby/JS bridge. To acquire the results the request must include a
  1783.  * key of oncomplete or onsuccess naming a valid callback function. The
  1784.  * length to format should be provided in the 'length' key of the request.
  1785.  * @param {Object} request An object providing a 'length' key containing
  1786.  *     the length to format and one or more optional callback specifiers per
  1787.  *     Ruby/JS bridge specs.
  1788.  */
  1789. su.formatLength = function(request) {
  1790.   su.callRuby('pull_format_length', request);
  1791. };
  1792.  
  1793. /**
  1794.  * Responds to notification that a call for Sketchup information has been
  1795.  * completed successfully.
  1796.  * @param {string} queryid The unique ID of the invocation used to call
  1797.  *     Ruby for Sketchup data.
  1798.  */
  1799. su.handlePullInformationSuccess = function(queryid) {
  1800.   su.info = su.getRubyResponse(queryid);
  1801.   su.strings = su.info.strings;
  1802. };
  1803.  
  1804. /**
  1805.  * Responds to notification that a call for Sketchup attribute report has
  1806.  * been received. Used by the following wrapper functions:
  1807.  *     su.activeModel.getDynamicAttributes
  1808.  *     su.activeModel.seletion.getDynamicAttributes
  1809.  * @param {string} queryid The unique ID of the invocation used to call Ruby
  1810.  *     Ruby for Sketchup data.
  1811.  */
  1812. su.handleWrapperSuccess = function(queryid) {
  1813.   var obj = su.getRubyResponse(queryid);
  1814.   su.wrapperOnSuccess_(obj);
  1815. };
  1816.  
  1817. /**
  1818.  * Responds to notification of successful translation of a set of strings.
  1819.  * This routine is responsible for loading the translation dictionary used
  1820.  * by the single-string method translateString.
  1821.  * @param {string} queryid The unique ID of the invocation used to call Ruby
  1822.  *     for translation data.
  1823.  */
  1824. su.handlePullTranslationsSuccess = function(queryid) {
  1825.   su.strings = su.getRubyResponse(queryid);
  1826. };
  1827.  
  1828. /**
  1829.  * Returns a single-quoted version of the input value with any embedded single
  1830.  * quotes escaped.
  1831.  * @param {Object} aValue The object whose string value should be quoted.
  1832.  * @return {string} The quoted string value.
  1833.  */
  1834. su.quote = function(aValue) {
  1835.   var str;
  1836.  
  1837.   if (aValue === null) {
  1838.     str = 'null';
  1839.   } else if (aValue === undefined) {
  1840.     str = 'undefined';
  1841.   } else if (su.canCall(aValue, 'toString')) {
  1842.     str = aValue.toString();
  1843.   } else {
  1844.     // Even though su.canCall(aValue, 'toString') should work in all cases,
  1845.     // it does not. Converting via concatenation fixes a bug that can throw
  1846.     // errors on IE.
  1847.     str = aValue + '';
  1848.   }
  1849.  
  1850.   return "'" + str.replace(/'/g, "\\'") + "'";
  1851. };
  1852.  
  1853. /**
  1854.  * Translates a string by checking a local dictionary of string values.
  1855.  * Lookup data is populated from Ruby via the pull_translations API call.
  1856.  * @param {string} text The string to translate.
  1857.  * @return {string} The translated text.
  1858.  */
  1859. su.translateString = function(text) {
  1860.   var str;
  1861.  
  1862.   if (su.isValid(su.strings)) {
  1863.     str = su.strings[text];
  1864.     if (su.isEmpty(str)) {
  1865.       // Look for a match on a string with quotes encoded. This is to handle
  1866.       // the fact that we encode this character when it goes over the bridge.
  1867.       str = su.strings[text.replace(/\"/g, '"')];
  1868.     }
  1869.   }
  1870.  
  1871.   return su.notEmpty(str) ? str : text;
  1872. };
  1873.  
  1874. /**
  1875.  * Removes leading and trailing whitespace of any kind from aString.
  1876.  * @param {string} aString The string to trim.
  1877.  * @return {string} A string with no leading/trailing whitespace.
  1878.  */
  1879. su.trimWhitespace = function(aString) 
  1880. {
  1881.   var str = aString.replace(/^\s\s*/, '');
  1882.   var ws = /\s/;
  1883.   var i = str.length;
  1884.   while (ws.test(str.charAt(--i))) {
  1885.   }
  1886.   return str.slice(0, i + 1);
  1887. };
  1888.  
  1889. /**
  1890.  * Truncates a String to a certain length (if longer than that length) and
  1891.  * adds an ellipsis.
  1892.  * @param {string} aString The string to truncate.
  1893.  * @param {number} aLength The length to truncate the string to.
  1894.  * @return {string} The string truncated to the length given.
  1895.  */
  1896. su.truncate = function(aString, aLength) 
  1897. {
  1898.   if (aString.length <= aLength) {
  1899.     return aString;
  1900.   }
  1901.  
  1902.   return aString.substr(0, aLength - 3) + '...';
  1903. };
  1904.  
  1905. /**
  1906.  * Unescapes HTML entities, converting '<' into a less-than symbol etc.
  1907.  * @param {string} text The string to unescape.
  1908.  * @return {string} The unescaped text.
  1909.  */
  1910. su.unescapeHTML = function(text) {
  1911.   // Force convert to a string.
  1912.   text = text + '';
  1913.   return text.replace(/&/g, '&'
  1914.     ).replace(/"/g, '"'
  1915.     ).replace(/'/g, "'"
  1916.     ).replace(/</g, '<'
  1917.     ).replace(/>/g, '>'
  1918.     ).replace(/ /g, ' '
  1919.     ).replace(/\/g, '\\'
  1920.     ).replace(/^\s*(.*)\s$/, '$1');
  1921. };
  1922.  
  1923. /**
  1924.  * Returns a url-encoded string built from the text provided. The string's
  1925.  * content is escaped to ensure that after SketchUp processes the string the
  1926.  * proper escapes remain.
  1927.  * @param {string} text The text to encode.
  1928.  * @return {string} The url-encoded string.
  1929.  */
  1930. su.urlEncode = function(text) {
  1931.   if (su.isEmpty(text)) {
  1932.     return '';
  1933.   }
  1934.  
  1935.   // We need to double encode the string so that it reaches SU in encoded
  1936.   // form, otherwise SU automatically unencodes strings leading to incorrect
  1937.   // parsing if the attribute values contain ampersands or equals.
  1938.   var str = encodeURIComponent(text);
  1939.  
  1940.   // The javascript escape function does not escape + symbols, so do that
  1941.   // manually.
  1942.   str = str.replace(/\+/g, '%2B');
  1943.  
  1944.   return str;
  1945. };
  1946.  
  1947. /**
  1948.  * Takes an object of name/value pairs and converts it to a properly encoded
  1949.  * query string.
  1950.  * @param {string|Object} params An object whose keys and values should be
  1951.  *     formatted into a URL query string.
  1952.  * @return {string} The value after the set operation has completed.
  1953.  */
  1954. su.createQueryString = function(params) {
  1955.   var qs = '';
  1956.   for (var key in params) {
  1957.     var value = params[key];
  1958.     value = su.urlEncode(value);
  1959.     qs += key + '=' + value + '&';
  1960.   }
  1961.  
  1962.   return qs.slice(0, -1);
  1963. };
  1964.  
  1965. /**
  1966.  * Returns a sanitized version of aString, meaning any embedded script tags
  1967.  * and similarly risky elements have been removed.
  1968.  * @param {string} aString The HTML string to sanitize.
  1969.  * @return {string} A nice shiny HTML string.
  1970.  */
  1971. su.sanitizeHTML = function(aString) {
  1972.  
  1973.   var str = aString.toString();
  1974.  
  1975.   // Strip out null.
  1976.   str = str.replace(/\[0x00\]/gmi,'');
  1977.  
  1978.   // Strip out carriage returns and other control characters.
  1979.   str = str.replace(/( | | | )/gmi,'');
  1980.   str = str.replace(/( ||\t|\n|\r)/gmi,'');
  1981.  
  1982.   // Strip out tags that do not close.
  1983.   str = str.replace(/<[^>]*$/gmi,'');
  1984.  
  1985.   // Strip out tags that do not open.
  1986.   str = str.replace(/^[^<]*>/gmi,'');
  1987.   
  1988.   // Check all instances of HTML tags. Only if they match our very limited
  1989.   // white list will they be allowed through.
  1990.   return str.replace(/<[^>]*>/gmi, function(match) {
  1991.       // If there are *any* style or javascript strings inside the tag,
  1992.       // then strip it. Also, look for any open parenthesis (escaped or 
  1993.       // unescaped), curly braces, or square backets, since simple link URLs
  1994.       // will not contain these whereas javascript will.
  1995.       var containsStyleRegex = new RegExp('style\s*|\\(|)|<.*<' +
  1996.           '|script:|file:|ftp:|(|\{|\}|\[|\]|%5B|%5D|%3C|%3E|(','img');
  1997.       if (containsStyleRegex.test(match) == true) {
  1998.         return '';
  1999.       }
  2000.  
  2001.       // If it's an http: or https: link or a font tag, then let it through.
  2002.       var isLinkOrFontRegex = new RegExp('^<\/*(a href=("|")http|font)','img');
  2003.       if (isLinkOrFontRegex.test(match) == true) {
  2004.         // If there is any attribute that starts with "on", then strip the 
  2005.         // tag, since this could be a binding to a JS event.
  2006.         var containsJSBinding = new RegExp('on\\S*\\s*=','img');
  2007.         if (containsJSBinding.test(match) == true) {
  2008.           return '';
  2009.         } else {
  2010.           return match;
  2011.         }
  2012.       }
  2013.  
  2014.       // Finally, only allow it if it's in our explicit white list.
  2015.       var whiteListRegEx = new RegExp('^</*' +
  2016.           '(b|i|u|strong|em|p|br|ol|ul|li|a)/*>$', 'img');
  2017.       if (whiteListRegEx.test(match) == true) {
  2018.         return match;
  2019.       } else {
  2020.         return '';
  2021.       }
  2022.       
  2023.     });
  2024. };
  2025.  
  2026. //  --------------------------------------------------------------------------
  2027. //  Key/Value Management
  2028. //  --------------------------------------------------------------------------
  2029.  
  2030. /**
  2031.  * Returns an Array of the items in the object in key/value Array pairs.
  2032.  * @param {Object} anObject The object whose items you want to acquire.
  2033.  * @return {Array} The list of keys/value pairs in the target object.
  2034.  */
  2035. su.getItems = function(anObject) {
  2036.   var arr = [];
  2037.   for (var i in anObject) {
  2038.     arr.push([i, anObject[i]]);
  2039.   }
  2040.   return arr;
  2041. };
  2042.  
  2043. /**
  2044.  * Returns an Array of the keys in the object.
  2045.  * @param {Object} anObject The object whose keys you want to acquire.
  2046.  * @return {Array} The list of keys in the target object.
  2047.  */
  2048. su.getKeys = function(anObject) {
  2049.   var arr = [];
  2050.   for (var i in anObject) {
  2051.     arr.push(i);
  2052.   }
  2053.   return arr;
  2054. };
  2055.  
  2056. /**
  2057.  * Returns an Array of the values in the object.
  2058.  * @param {object} anObject The object whose values you want to acquire.
  2059.  * @return {Array} The list of values in the target object.
  2060.  */
  2061. su.getValues = function(anObject) {
  2062.   var arr = [];
  2063.   for (var i in anObject) {
  2064.     arr.push(anObject[i]);
  2065.   }
  2066.   return arr;
  2067. };
  2068.  
  2069. //  --------------------------------------------------------------------------
  2070. //  Selection Management
  2071. //  --------------------------------------------------------------------------
  2072.  
  2073. /**
  2074.  * Returns the string value of an element's current selected text. If no
  2075.  * element or ID is provided then the current active element is used.
  2076.  * @param {string|Element} elementOrID The element or element ID to find.
  2077.  * @return {string} The selection string value.
  2078.  */
  2079. su.getTextSelection = function(elementOrID) {
  2080.   var el;
  2081.   var selection;
  2082.   var text;
  2083.  
  2084.   if (su.notEmpty(elementOrID)) {
  2085.     el = $(elementOrID);
  2086.   } else {
  2087.     el = document.activeElement;
  2088.   }
  2089.  
  2090.   //  if the element isn't the active element then we aren't looking in the
  2091.   //  same place and the selection in that element is empty.
  2092.   if (!el || (el != document.activeElement)) {
  2093.     return '';
  2094.   }
  2095.  
  2096.   if (su.IS_MAC) {
  2097.     text = el.value.substring(el.selectionStart, el.selectionEnd);
  2098.   } else {
  2099.     if (su.isValid(selection = document.selection)) {
  2100.       text = selection.createRange().text;
  2101.     }
  2102.   }
  2103.  
  2104.   return text || '';
  2105. };
  2106.  
  2107. /**
  2108.  * Replaces the current selection in a text field or text area with the text
  2109.  * provided.
  2110.  * @param {string|Element} elementOrID The element or element ID to find.
  2111.  * @param {string} text The new text to replace/insert.
  2112.  * @param {string} textRangeToReplace Optional. An explicit range to replace.
  2113.  */
  2114. su.replaceSelection = function(elementOrID, text, textRangeToReplace) {
  2115.   var el = $(elementOrID)
  2116.   if (su.notValid(el)) {
  2117.     return;
  2118.   }
  2119.  
  2120.   if (su.IS_MAC) {
  2121.     if (su.isValid(el.selectionStart)) {
  2122.       var start = el.selectionStart;
  2123.       var end = el.selectionEnd;
  2124.       el.value = el.value.substr(0, start) + (text || '') +
  2125.         el.value.substr(end);
  2126.       el.selectionStart = start + text.length;
  2127.       el.selectionEnd = start + text.length;
  2128.     }
  2129.   } else {
  2130.     if (su.isValid(textRangeToReplace)) {
  2131.       el.focus();
  2132.       textRangeToReplace.text = text;
  2133.     } else {
  2134.       el.focus();
  2135.       var sel = document.selection.createRange();
  2136.       sel.text = text;
  2137.     }
  2138.   }
  2139. };
  2140.  
  2141. /**
  2142.  * Sets the current selection in a text field or text area to the range of
  2143.  * indexes provided.
  2144.  * @param {string|Element} elementOrID The element or element ID to find.
  2145.  * @param {number} startIndex The starting index from 0 to the field length.
  2146.  * @param {number} endIndex The ending index from 0 to the field length.
  2147.  */
  2148. su.selectFromTo = function(elementOrID, startIndex, endIndex) {
  2149.   var el = $(elementOrID);
  2150.   if (su.notValid(el)) {
  2151.     // Note that the $() function will raise InvalidParameter here, so all
  2152.     // we need to do is return.
  2153.     return;
  2154.   }
  2155.  
  2156.   // Normalize the indexes so they relate to the text content. Both start
  2157.   // and end must be greater than or equal to 0 and less than or equal to
  2158.   // the field length. End must be after, or equal to, the start.
  2159.   var start = Math.max(startIndex || 0, 0);
  2160.   var end = Math.max(endIndex || 0, 0);
  2161.   var len = el.value.length;
  2162.   start = Math.min(start, len);
  2163.   end = Math.min(end, len);
  2164.   end = Math.max(end, start);
  2165.  
  2166.   if (su.IS_MAC) {
  2167.     el.setSelectionRange(start, end);
  2168.   } else {
  2169.     var range = el.createTextRange();
  2170.     if (su.isValid(range)) {
  2171.       range.collapse(true);
  2172.       range.moveStart('character', start);
  2173.       range.moveEnd('character', end - start);
  2174.       range.select();
  2175.     }
  2176.   }
  2177. };
  2178.  
  2179. //  --------------------------------------------------------------------------
  2180. //  Status Information
  2181. //  --------------------------------------------------------------------------
  2182.  
  2183. /**
  2184.  * Updates the su.IS_ONLINE flag by calling SketchUp and then invokes the
  2185.  * named callback function if the online status is false.
  2186.  * @param {string} callback The name of the function to call if offline.
  2187.  */
  2188. su.ifOffline = function(callback) {
  2189.   if (!su.isString(callback)) {
  2190.     su.raise(su.translateString('InvalidParameter'));
  2191.     return;
  2192.   }
  2193.  
  2194.   su.callRuby('is_online', {'onsuccess': 'su.handleOnlineUpdate_',
  2195.     'oncomplete': 'su.handleIsOffline_',
  2196.     'callback': callback});
  2197. };
  2198.  
  2199. /**
  2200.  * Updates the su.IS_ONLINE flag by calling SketchUp and then invokes the
  2201.  * named callback function if the online status is true.
  2202.  * @param {string} callback The name of the function to call if online.
  2203.  */
  2204. su.ifOnline = function(callback) {
  2205.   if (!su.isString(callback)) {
  2206.     su.raise(su.translateString('InvalidParameter'));
  2207.     return;
  2208.   }
  2209.  
  2210.   su.callRuby('is_online', {'onsuccess': 'su.handleOnlineUpdate_',
  2211.     'oncomplete': 'su.handleIsOnline_',
  2212.     'callback': callback});
  2213. };
  2214.  
  2215. /**
  2216.  * Responds to notifications from the is_online Ruby call regarding online
  2217.  * status. The resulting value is stored in the su.IS_ONLINE flag for
  2218.  * reference.
  2219.  * @param {string} queryid The ID of the query used to query online state.
  2220.  * @private
  2221.  */
  2222. su.handleOnlineUpdate_ = function(queryid) {
  2223.   var obj = su.getRubyResponse(queryid);
  2224.   if (su.isValid(obj)) {
  2225.     su.IS_ONLINE = obj['online'];
  2226.   }
  2227. };
  2228.  
  2229. /**
  2230.  * Responds to notifications that SketchUp has network connectivity.
  2231.  * @param {string} queryid The ID of the query used to query online state.
  2232.  * @private
  2233.  */
  2234. su.handleIsOnline_ = function(queryid) {
  2235.   var request = su.getRubyRequest(queryid);
  2236.   if (su.isValid(request)) {
  2237.     var callback = request['callback'];
  2238.     if (su.notEmpty(callback)) {
  2239.       var obj = su.resolveObjectPath(callback);
  2240.       if (su.isFunction(obj)) {
  2241.         obj(queryid);
  2242.       }
  2243.     }
  2244.   }
  2245. };
  2246.  
  2247. //  --------------------------------------------------------------------------
  2248. //  Window Management
  2249. //  --------------------------------------------------------------------------
  2250.  
  2251. /**
  2252.  * Sets the 'top', 'left', 'width', and 'height' properties of the current
  2253.  * dialog running the code. An optional 'dialog' key can supply the desired
  2254.  * dialog name.
  2255.  * @param {object} params   An object whose keys should include top/left
  2256.  *                          and width/height pairs as needed to configure the
  2257.  *                          size and position of the dialog.
  2258.  */
  2259. su.setDialogProperties = function(params) {
  2260.   su.callRuby('set_dialog_properties', params);
  2261.   return;
  2262. };
  2263.  
  2264. //  --------------------------------------------------------------------------
  2265. //  Ruby/JavaScript "Bridge"
  2266. //  --------------------------------------------------------------------------
  2267.  
  2268. // The Ruby and JavaScript code in SketchUp communicates asynchronously via
  2269. // what we call the "ruby/js bridge", a set of coordinated functions in Ruby
  2270. // and JavaScript which consumers use to invoke Ruby from JavaScript and
  2271. // receive callback notifications and response/fault data.
  2272. //
  2273. // The public JS functions are su.callRuby, su.getRubyResponse,
  2274. // and su.getRubyFault. In addition, when making calls via the
  2275. // su.callRuby method a JavaScript developer can provide onsuccess,
  2276. // onfailure, and oncomplete keys which provide the names of JavaScript
  2277. // functions to invoke when the Ruby is complete, succeeded, or failed. The
  2278. // oncomplete hook, if provided, is always called regardless of success or
  2279. // failure.
  2280. //
  2281. // The remainer of the functions here are invoked via Ruby and shouldn't
  2282. // normally be invoked by JavaScript code. See the js_callback routine in Ruby
  2283. // for more information on how Ruby communicates with JavaScript oncomplete.
  2284.  
  2285. //  --------------------------------------------------------------------------
  2286.  
  2287. /**
  2288.  * Constant defining the key used to store fault data from a Ruby call.
  2289.  * @type {string}
  2290.  */
  2291. su.RUBY_FAULT = 'fault';
  2292.  
  2293. /**
  2294.  * Constant defining the key used for Ruby query data.
  2295.  * @type {string}
  2296.  */
  2297. su.RUBY_QUERY = 'query';
  2298.  
  2299. /**
  2300.  * Constant defining the key used to identify a specific Ruby query.
  2301.  * @type {string}
  2302.  */
  2303. su.RUBY_REQUEST = 'request';
  2304.  
  2305. /**
  2306.  * Constant defining the key used to store response data from a Ruby call.
  2307.  * @type {string}
  2308.  */
  2309. su.RUBY_RESPONSE = 'response';
  2310.  
  2311. /**
  2312.  * Container for all callback data cached for the Ruby/JS bridge.
  2313.  * @type {Object}
  2314.  * @private
  2315.  */
  2316. su.rubyCallData_ = {};
  2317.  
  2318. /**
  2319.  * The name of the last Ruby function invoked.
  2320.  * @type {string?}
  2321.  * @private
  2322.  */
  2323. su.rubyLastCall_ = null;
  2324.  
  2325. //  --------------------------------------------------------------------------
  2326.  
  2327. /**
  2328.  * Invokes a function in Ruby defined as part of the SketchUp Ruby API or as
  2329.  * part of an included/required Ruby module. Note that this call is made in
  2330.  * an asynchronous fashion. Callbacks to the JavaScript are dependent on the
  2331.  * Ruby function being invoked. See SketchUp's js_callback Ruby method for
  2332.  * more information on how to return results to the invoking JavaScript.
  2333.  * @param {string} funcname The name of the Ruby function to invoke.
  2334.  * @param {string|Object} opt_request A pre-formatted URL-style query string
  2335.  *     or an object whose keys and values should be formatted into a URL
  2336.  *     query string.
  2337.  */
  2338. su.callRuby = function(funcname, opt_request) {
  2339.   var query;
  2340.  
  2341.   if (su.isEmpty(funcname)) {
  2342.     return;
  2343.   }
  2344.  
  2345.   // Create a unique ID for this particular call (as long as we don't call
  2346.   // the same function within the ms clock threshold these will be unique).
  2347.   var queryid = funcname + '_' + (new Date()).getTime();
  2348.  
  2349.   // Save the original request object itself, without alteration.
  2350.   su.setRubyRequest_(queryid, opt_request);
  2351.  
  2352.   // Construct a viable query string version of the request data.
  2353.   if (su.isString(opt_request)) {
  2354.     query = opt_request;
  2355.   } else if (su.isValid(opt_request)) {
  2356.     query = su.createQueryString(opt_request);
  2357.   }
  2358.   su.setRubyQuery_(queryid, query);
  2359.  
  2360.   // Build an element we can access from the Ruby side to get the data.
  2361.   try {
  2362.     var elem = document.createElement('input');
  2363.     elem.setAttribute('type', 'hidden');
  2364.     elem.setAttribute('id', queryid);
  2365.     elem.setAttribute('style', 'display:none');
  2366.     elem.value = query;
  2367.     document.body.appendChild(elem);
  2368.   } catch (e) {
  2369.     // If the element version fails we have to fall back to passing via the
  2370.     // normal query string approach. We'll signify that by leaving queryid
  2371.     // empty and appending the original query string.
  2372.     queryid = '&' + query;
  2373.   }
  2374.  
  2375.   // Note the use of a non-standard scheme here. This is installed by
  2376.   // SketchUp when it embeds the browser, allowing it to intercept all skp:
  2377.   // prefixed URIs and to route them to the Ruby SketchUp interface.
  2378.   var url = 'skp:' + funcname;
  2379.   url += '@queryid=' + queryid;
  2380.  
  2381.   su.rubyLastCall_ = queryid;
  2382.   su.setLocation(url);
  2383. };
  2384.  
  2385. /**
  2386.  * Sets the current location to the URI provided.
  2387.  * @param {string} url The url to set as the current location href.
  2388.  */
  2389. su.setLocation = function(url) {
  2390.   // Actual call is made here by setting location to our skp: url but we do
  2391.   // that in a setTimeout to force a flush of the DOM before Ruby invocation.
  2392.   window.setTimeout(function() {
  2393.     window.location.href = url;
  2394.   }, 0);
  2395. };
  2396.  
  2397. /**
  2398.  * Stores a value to the document's cookie using the name supplied.
  2399.  * @param {string} name The name to store the value under.
  2400.  * @param {string} value The value to store the value under.
  2401.  */
  2402. su.storeToCookie = function(name, value) {
  2403.   var cookies = document.cookie;
  2404.   var newCookies;
  2405.  
  2406.   // Pull apart the cookie, capturing the value for the named key up through
  2407.   // the terminating semi-colon or the end of the string.
  2408.   var cookieRegex = new RegExp('(.*)' + name + '=(.+?)(;|$)');
  2409.  
  2410.   if (cookieRegex.test(cookies) == true) {
  2411.     newCookies = cookies.replace(cookieRegex,
  2412.      '$1' + name + '=' + value + ';$3');
  2413.   } else {
  2414.     newCookies = name + '=' + value + ';' + cookies;
  2415.   }
  2416.  
  2417.   document.cookie = newCookies;
  2418. };
  2419.  
  2420. /**
  2421.  * Retrieves a value from the document's cookie using the name supplied.
  2422.  * @param {string} name The name to retrieve the value from.
  2423.  * @return {string} The cookie value stored under the name key provided.
  2424.  */
  2425. su.retrieveFromCookie = function(name) {
  2426.   var cookies = document.cookie;
  2427.  
  2428.   var cookieRegex = new RegExp('(.*)' + name + '=(.+?)(;|$)');
  2429.  
  2430.   if (cookieRegex.test(cookies) == true) {
  2431.     return cookies.match(cookieRegex)[2];
  2432.   }
  2433.  
  2434.   return null;
  2435. };
  2436.  
  2437. /**
  2438.  * Called by Ruby to clear Ruby/JS bridge callback data values so that no
  2439.  * callback data is left over from a prior call. You should never need to
  2440.  * call this method from JavaScript.
  2441.  * @param {string} queryid The unique ID of the Ruby function invocation
  2442.  *     whose data should be cleared.
  2443.  * @private
  2444.  */
  2445. su.clearRubyData_ = function(queryid) {
  2446.   var name = queryid || su.rubyLastCall_;
  2447.   try {
  2448.     var elem = $(name);
  2449.     if (su.isValid(elem)) {
  2450.       elem.parentNode.removeChild(elem);
  2451.     }
  2452.   } catch (e) {
  2453.   } finally {
  2454.     // Remove the data structures for the call in question.
  2455.     delete su.rubyCallData_[name];
  2456.   }
  2457. };
  2458.  
  2459. /**
  2460.  * Returns a particular piece of data returned from the last callRuby call.
  2461.  * If a queryid is provided then the return value is specific to that Ruby
  2462.  * function invocation.
  2463.  * @param {string} queryid The unique ID of the invocation whose results
  2464.  *     we're interested in. Default is the last call.
  2465.  * @param {string} key The specific data key being requested. This is
  2466.  *     commonly either su.RUBY_FAULT or su.RUBY_RESPONSE.
  2467.  * @return {Object?} Response data from the last callRuby invocation.
  2468.  * @private
  2469.  */
  2470. su.getRubyData_ = function(queryid, key) {
  2471.   var name = queryid || su.rubyLastCall_;
  2472.   var data = su.rubyCallData_[name];
  2473.  
  2474.   if (su.isValid(data)) {
  2475.     if (su.isEmpty(key)) {
  2476.       return data;
  2477.     }
  2478.  
  2479.     var obj = data[key];
  2480.     if (su.isString(obj) && su.notEmpty(obj)) {
  2481.       try {
  2482.         var str = obj.replace(/\%22/gi, '"');
  2483.         str = str.replace(/\n/gi, '\\n');
  2484.         str = str.replace(/^\s(.*)\s$/, '$1');
  2485.         obj = eval('(' + str + ')');
  2486.       } catch (e) {
  2487.         // If the string looks like it was intended to be valid JSON then
  2488.         // we'll notify via warning that an apparent parser error happened.
  2489.         if ((/^\{(?:.*)\}$/).test(str)) {
  2490.           su.log(su.translateString('WARNING: Unable to parse: ') + str);
  2491.         }
  2492.         // In either case we return the original string and let the requestor
  2493.         // deal with any fallout since at least it retains debugg-ability.
  2494.         obj = str;
  2495.       }
  2496.     }
  2497.   }
  2498.  
  2499.   return obj;
  2500. };
  2501.  
  2502. /**
  2503.  * Returns any fault object returned from the last callRuby invocation. If
  2504.  * a queryid is provided then the return value is specific to that Ruby
  2505.  * function invocation.
  2506.  * @param {string} queryid The unique ID of the invocation whose results
  2507.  *     we're interested in. Default is the last call.
  2508.  * @return {Object?} Fault data from the last callRuby invocation.
  2509.  */
  2510. su.getRubyFault = function(queryid) {
  2511.   return su.getRubyData_(queryid, su.RUBY_FAULT);
  2512. };
  2513.  
  2514. /**
  2515.  * Returns any query string used during the last callRuby invocation. If a
  2516.  * queryid is provided then the return value is specific to that Ruby function
  2517.  * invocation.
  2518.  * @param {string} queryid The unique ID of the invocation whose results
  2519.  *     we're interested in. Default is the last call.
  2520.  * @return {string?} The query string from the last callRuby invocation.
  2521.  */
  2522. su.getRubyQuery = function(queryid) {
  2523.   return su.getRubyData_(queryid, su.RUBY_QUERY);
  2524. };
  2525.  
  2526. /**
  2527.  * Returns any query request object used during the last callRuby invocation.
  2528.  * If a queryid is provided then the return value is specific to that Ruby
  2529.  * function invocation.
  2530.  * @param {string} queryid The unique ID of the invocation whose results
  2531.  *     we're interested in. Default is the last call.
  2532.  * @return {Object?} Request data from the last callRuby invocation.
  2533.  */
  2534. su.getRubyRequest = function(queryid) {
  2535.   return su.getRubyData_(queryid, su.RUBY_REQUEST);
  2536. };
  2537.  
  2538. /**
  2539.  * Returns a result object constructed from the return value from the last
  2540.  * callRuby invocation. If a queryid is provided then the response data is
  2541.  * specific to that Ruby function invocation.
  2542.  * @param {string} queryid The unique ID of the invocation whose results
  2543.  *     we're interested in. Default is the last call.
  2544.  * @return {Object?} Response data from the last callRuby invocation.
  2545.  */
  2546. su.getRubyResponse = function(queryid) {
  2547.   return su.getRubyData_(queryid, su.RUBY_RESPONSE);
  2548. };
  2549.  
  2550. /**
  2551.  * The primary callback entry point from Ruby back into JavaScript. This
  2552.  * method is invoked from Ruby to provide common error trapping and logging
  2553.  * functionality around all JS callbacks.
  2554.  * @param {string} callback The name of the callback function to invoke.
  2555.  * @param {string} queryid The unique ID of the originating function the
  2556.  *     callback was registered for.
  2557.  * @private
  2558.  */
  2559. su.rubyCallback_ = function(callback, queryid) {
  2560.   // Find the function reference, dealing with namespaces as needed.
  2561.   var obj = su.resolveObjectPath(callback);
  2562.   if (su.isFunction(obj) != true) {
  2563.     su.raise(su.translateString('Missing callback function: ') + callback);
  2564.       return;
  2565.     }
  2566.  
  2567.   try {
  2568.     if (!su.isFunction(obj)) {
  2569.       su.raise(su.translateString('Missing callback function: ') + callback);
  2570.       return;
  2571.     }
  2572.     obj(queryid);
  2573.   } catch (e) {
  2574.     su.raise(su.translateString('Callback function error: ') + e.message);
  2575.   }
  2576. };
  2577.  
  2578. /**
  2579.  * Updates a Ruby/JS bridge field using the key and value provided. This
  2580.  * method is invoked by other Ruby-initiated bridge methods. You should
  2581.  * never need to call this method from JavaScript.
  2582.  * @param {string} queryid The unique ID of the invocation whose results
  2583.  *     are being set. Default is the last call.
  2584.  * @param {string} key The name of the key. This is commonly either
  2585.  *     su.RUBY_FAULT or su.RUBY_RESPONSE.
  2586.  * @param {Object} value The value to associate with the key.
  2587.  * @return {Object?} The value after the set operation has completed.
  2588.  * @private
  2589.  */
  2590. su.setRubyData_ = function(queryid, key, value) {
  2591.   var name = queryid || su.rubyLastCall_;
  2592.   var data = su.rubyCallData_[name];
  2593.  
  2594.   if (su.notValid(data)) {
  2595.     data = {};
  2596.     su.rubyCallData_[name] = data;
  2597.   }
  2598.  
  2599.   data[key] = value;
  2600.  
  2601.   return data[key];
  2602. };
  2603.  
  2604. /**
  2605.  * Called by Ruby to update the Ruby/JS bridge fault field value with a
  2606.  * unique fault code or identifier. When a fault is encountered in a Ruby
  2607.  * function this bridge method is invoked to pass that information back to
  2608.  * JavaScript. You should never need to call this method from JavaScript.
  2609.  * @param {string} queryid The unique ID of the invocation whose fault
  2610.  *     is being set. Default is the last call.
  2611.  * @param {string} value The fault identifier.
  2612.  * @return {Object?} The value after the set operation has completed.
  2613.  * @private
  2614.  */
  2615. su.setRubyFault_ = function(queryid, value) {
  2616.   return su.setRubyData_(queryid, su.RUBY_FAULT, value);
  2617. };
  2618.  
  2619. /**
  2620.  * Called by Ruby to update the Ruby/JS bridge query field value. When a
  2621.  * call is made across the bridge the actual query data is placed in the
  2622.  * query field rather than trying to pass it on the URL to avoid length
  2623.  * issues in different browsers.
  2624.  * @param {string} queryid The unique ID of the invocation whose query
  2625.  *     is being set. Default is the last call.
  2626.  * @param {string} value The query string content.
  2627.  * @return {Object?} The value after the set operation has completed.
  2628.  * @private
  2629.  */
  2630. su.setRubyQuery_ = function(queryid, value) {
  2631.   return su.setRubyData_(queryid, su.RUBY_QUERY, value);
  2632. };
  2633.  
  2634. /**
  2635.  * Called by Ruby to update the Ruby/JS bridge request object value with
  2636.  * request data from a callRuby method. Data in this field can be acquired
  2637.  * from the JavaScript side by invoking su.getRubyRequest(). You never call
  2638.  * this method from JavaScript.
  2639.  * @param {string} queryid The unique ID of the invocation whose request
  2640.  *     data is being set. Default is the last call.
  2641.  * @param {Object} value The request data. This is the object used to pass
  2642.  *     initial query parameter data to a callRuby function.
  2643.  * @return {Object?} The value after the set operation has completed.
  2644.  * @private
  2645.  */
  2646. su.setRubyRequest_ = function(queryid, value) {
  2647.   return su.setRubyData_(queryid, su.RUBY_REQUEST, value);
  2648. };
  2649.  
  2650. /**
  2651.  * Called by Ruby to update the Ruby/JS bridge response field value with
  2652.  * result data from a callRuby method. Data in this field can be acquired
  2653.  * from the JavaScript side by invoking su.getRubyResponse(). You never call
  2654.  * this method from JavaScript.
  2655.  * @param {string} queryid The unique ID of the invocation whose response
  2656.  *     data is being set. Default is the last call.
  2657.  * @param {string} value The response data. This is often provided as
  2658.  *     a JSON-formatted string.
  2659.  * @return {Object?} The value after the set operation has completed.
  2660.  * @private
  2661.  */
  2662. su.setRubyResponse_ = function(queryid, value) {
  2663.   return su.setRubyData_(queryid, su.RUBY_RESPONSE, value);
  2664. };
  2665.  
  2666.