http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/focus.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/focus.js 
b/externs/GCL/externs/goog/editor/focus.js
new file mode 100644
index 0000000..4253b3c
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/focus.js
@@ -0,0 +1,32 @@
+// Copyright 2009 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilties to handle focusing related to rich text editing.
+ *
+ */
+
+goog.provide('goog.editor.focus');
+
+goog.require('goog.dom.selection');
+
+
+/**
+ * Change focus to the given input field and set cursor to end of current text.
+ * @param {Element} inputElem Input DOM element.
+ */
+goog.editor.focus.focusInputField = function(inputElem) {
+  inputElem.focus();
+  goog.dom.selection.setCursorPosition(inputElem, inputElem.value.length);
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/icontent.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/icontent.js 
b/externs/GCL/externs/goog/editor/icontent.js
new file mode 100644
index 0000000..fac433a
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/icontent.js
@@ -0,0 +1,300 @@
+// Copyright 2007 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// All Rights Reserved.
+
+/**
+ * @fileoverview Static functions for writing the contents of an iframe-based
+ * editable field. These vary significantly from browser to browser. Uses
+ * strings and document.write instead of DOM manipulation, because
+ * iframe-loading is a performance bottleneck.
+ *
+ * @author [email protected] (Nick Santos)
+ */
+
+goog.provide('goog.editor.icontent');
+goog.provide('goog.editor.icontent.FieldFormatInfo');
+goog.provide('goog.editor.icontent.FieldStyleInfo');
+
+goog.require('goog.dom');
+goog.require('goog.editor.BrowserFeature');
+goog.require('goog.style');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * A data structure for storing simple rendering info about a field.
+ *
+ * @param {string} fieldId The id of the field.
+ * @param {boolean} standards Whether the field should be rendered in
+ *     standards mode.
+ * @param {boolean} blended Whether the field is in blended mode.
+ * @param {boolean} fixedHeight Whether the field is in fixedHeight mode.
+ * @param {Object=} opt_extraStyles Other style attributes for the field,
+ *     represented as a map of strings.
+ * @constructor
+ * @final
+ */
+goog.editor.icontent.FieldFormatInfo = function(fieldId, standards, blended,
+    fixedHeight, opt_extraStyles) {
+  this.fieldId_ = fieldId;
+  this.standards_ = standards;
+  this.blended_ = blended;
+  this.fixedHeight_ = fixedHeight;
+  this.extraStyles_ = opt_extraStyles || {};
+};
+
+
+
+/**
+ * A data structure for storing simple info about the styles of a field.
+ * Only needed in Firefox/Blended mode.
+ * @param {Element} wrapper The wrapper div around a field.
+ * @param {string} css The css for a field.
+ * @constructor
+ * @final
+ */
+goog.editor.icontent.FieldStyleInfo = function(wrapper, css) {
+  this.wrapper_ = wrapper;
+  this.css_ = css;
+};
+
+
+/**
+ * Whether to always use standards-mode iframes.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.icontent.useStandardsModeIframes_ = false;
+
+
+/**
+ * Sets up goog.editor.icontent to always use standards-mode iframes.
+ */
+goog.editor.icontent.forceStandardsModeIframes = function() {
+  goog.editor.icontent.useStandardsModeIframes_ = true;
+};
+
+
+/**
+ * Generate the initial iframe content.
+ * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about
+ *     the field.
+ * @param {string} bodyHtml The HTML to insert as the iframe body.
+ * @param {goog.editor.icontent.FieldStyleInfo?} style Style info about
+ *     the field, if needed.
+ * @return {string} The initial IFRAME content HTML.
+ * @private
+ */
+goog.editor.icontent.getInitialIframeContent_ =
+    function(info, bodyHtml, style) {
+  var html = [];
+
+  if (info.blended_ && info.standards_ ||
+      goog.editor.icontent.useStandardsModeIframes_) {
+    html.push('<!DOCTYPE HTML>');
+  }
+
+  // <HTML>
+  // NOTE(user): Override min-widths that may be set for all
+  // HTML/BODY nodes. A similar workaround is below for the <body> tag. This
+  // can happen if the host page includes a rule like this in its CSS:
+  //
+  // html, body {min-width: 500px}
+  //
+  // In this case, the iframe's <html> and/or <body> may be affected. This was
+  // part of the problem observed in http://b/5674613. (The other part of that
+  // problem had to do with the presence of a spurious horizontal scrollbar,
+  // which caused the editor height to be computed incorrectly.)
+  html.push('<html style="background:none transparent;min-width:0;');
+
+  // Make sure that the HTML element's height has the
+  // correct value as the body element's percentage height is made relative
+  // to the HTML element's height.
+  // For fixed-height it should be 100% since we want the body to fill the
+  // whole height. For growing fields it should be auto since we want the
+  // body to size to its content.
+  if (info.blended_) {
+    html.push('height:', info.fixedHeight_ ? '100%' : 'auto');
+  }
+  html.push('">');
+
+  // <HEAD><STYLE>
+
+  // IE/Safari whitebox need styles set only iff the client specifically
+  // requested them.
+  html.push('<head><style>');
+  if (style && style.css_) {
+    html.push(style.css_);
+  }
+
+  // Firefox blended needs to inherit all the css from the original page.
+  // Firefox standards mode needs to set extra style for images.
+  if (goog.userAgent.GECKO && info.standards_) {
+    // Standards mode will collapse broken images.  This means that they
+    // can never be removed from the field.  This style forces the images
+    // to render as a broken image icon, sized based on the width and height
+    // of the image.
+    // TODO(user): Make sure we move this into a contentEditable code
+    // path if there ever is one for FF.
+    html.push(' img {-moz-force-broken-image-icon: 1;}');
+  }
+
+  html.push('</style></head>');
+
+  // <BODY>
+  // Hidefocus is needed to ensure that IE7 doesn't show the dotted, focus
+  // border when you tab into the field.
+  html.push('<body g_editable="true" hidefocus="true" ');
+  if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
+    html.push('contentEditable ');
+  }
+
+  html.push('class="editable ');
+
+  // TODO: put the field's original ID on the body and stop using ID as a
+  // way of getting the pointer to the field in the iframe now that it's
+  // always the body.
+  html.push('" id="', info.fieldId_, '" style="min-width:0;');
+
+  if (goog.userAgent.GECKO && info.blended_) {
+    // IMPORTANT: Apply the css from the body then all of the clearing
+    // CSS to make sure the clearing CSS overrides (e.g. if the body
+    // has a 3px margin, we want to make sure to override it with 0px.
+    html.push(
+
+        // margin should not be applied to blended mode because the margin is
+        // outside the iframe
+        // In whitebox mode, we want to leave the margin to the default so
+        // there is a nice margin around the text.
+        ';width:100%;border:0;margin:0;background:none transparent;',
+
+        // In standards-mode, height 100% makes the body size to its
+        // parent html element, but in quirks mode, we want auto because
+        // 100% makes it size to the containing window even if the html
+        // element is smaller.
+        // TODO: Fixed height, standards mode, CSS_WRITING, with margins on the
+        // paragraphs has a scrollbar when it doesn't need it.  Putting the
+        // height to auto seems to fix it.  Figure out if we should always
+        // just use auto?
+        ';height:', info.standards_ ? '100%' : 'auto');
+
+    // Only do this for mozilla. IE6 standards mode has a rendering bug when
+    // there are scrollbars and the body's overflow property is auto
+    if (info.fixedHeight_) {
+      html.push(';overflow:auto');
+    } else {
+      html.push(';overflow-y:hidden;overflow-x:auto');
+    }
+  }
+
+  // Hide the native focus rect in Opera.
+  if (goog.userAgent.OPERA) {
+    html.push(';outline:hidden');
+  }
+
+  for (var key in info.extraStyles_) {
+    html.push(';' + key + ':' + info.extraStyles_[key]);
+  }
+
+  html.push('">', bodyHtml, '</body></html>');
+
+  return html.join('');
+};
+
+
+/**
+ * Write the initial iframe content in normal mode.
+ * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about
+ *     the field.
+ * @param {string} bodyHtml The HTML to insert as the iframe body.
+ * @param {goog.editor.icontent.FieldStyleInfo?} style Style info about
+ *     the field, if needed.
+ * @param {HTMLIFrameElement} iframe The iframe.
+ */
+goog.editor.icontent.writeNormalInitialBlendedIframe =
+    function(info, bodyHtml, style, iframe) {
+  // Firefox blended needs to inherit all the css from the original page.
+  // Firefox standards mode needs to set extra style for images.
+  if (info.blended_) {
+    var field = style.wrapper_;
+    // If there is padding on the original field, then the iFrame will be
+    // positioned inside the padding by default.  We don't want this, as it
+    // causes the contents to appear to shift, and also causes the
+    // scrollbars to appear inside the padding.
+    //
+    // To compensate, we set the iframe margins to offset the padding.
+    var paddingBox = goog.style.getPaddingBox(field);
+    if (paddingBox.top || paddingBox.left ||
+        paddingBox.right || paddingBox.bottom) {
+      goog.style.setStyle(iframe, 'margin',
+          (-paddingBox.top) + 'px ' +
+          (-paddingBox.right) + 'px ' +
+          (-paddingBox.bottom) + 'px ' +
+          (-paddingBox.left) + 'px');
+    }
+  }
+
+  goog.editor.icontent.writeNormalInitialIframe(
+      info, bodyHtml, style, iframe);
+};
+
+
+/**
+ * Write the initial iframe content in normal mode.
+ * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about
+ *     the field.
+ * @param {string} bodyHtml The HTML to insert as the iframe body.
+ * @param {goog.editor.icontent.FieldStyleInfo?} style Style info about
+ *     the field, if needed.
+ * @param {HTMLIFrameElement} iframe The iframe.
+ */
+goog.editor.icontent.writeNormalInitialIframe =
+    function(info, bodyHtml, style, iframe) {
+
+  var html = goog.editor.icontent.getInitialIframeContent_(
+      info, bodyHtml, style);
+
+  var doc = goog.dom.getFrameContentDocument(iframe);
+  doc.open();
+  doc.write(html);
+  doc.close();
+};
+
+
+/**
+ * Write the initial iframe content in IE/HTTPS mode.
+ * @param {goog.editor.icontent.FieldFormatInfo} info Formatting info about
+ *     the field.
+ * @param {Document} doc The iframe document.
+ * @param {string} bodyHtml The HTML to insert as the iframe body.
+ */
+goog.editor.icontent.writeHttpsInitialIframe = function(info, doc, bodyHtml) {
+  var body = doc.body;
+
+  // For HTTPS we already have a document with a doc type and a body element
+  // and don't want to create a new history entry which can cause data loss if
+  // the user clicks the back button.
+  if (goog.editor.BrowserFeature.HAS_CONTENT_EDITABLE) {
+    body.contentEditable = true;
+  }
+  body.className = 'editable';
+  body.setAttribute('g_editable', true);
+  body.hideFocus = true;
+  body.id = info.fieldId_;
+
+  goog.style.setStyle(body, info.extraStyles_);
+  body.innerHTML = bodyHtml;
+};
+

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/link.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/link.js 
b/externs/GCL/externs/goog/editor/link.js
new file mode 100644
index 0000000..72f6c52
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/link.js
@@ -0,0 +1,390 @@
+// Copyright 2007 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview A utility class for managing editable links.
+ *
+ * @author [email protected] (Nick Santos)
+ */
+
+goog.provide('goog.editor.Link');
+
+goog.require('goog.array');
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.Range');
+goog.require('goog.dom.TagName');
+goog.require('goog.editor.BrowserFeature');
+goog.require('goog.editor.Command');
+goog.require('goog.editor.node');
+goog.require('goog.editor.range');
+goog.require('goog.string');
+goog.require('goog.string.Unicode');
+goog.require('goog.uri.utils');
+goog.require('goog.uri.utils.ComponentIndex');
+
+
+
+/**
+ * Wrap an editable link.
+ * @param {HTMLAnchorElement} anchor The anchor element.
+ * @param {boolean} isNew Whether this is a new link.
+ * @constructor
+ * @final
+ */
+goog.editor.Link = function(anchor, isNew) {
+  /**
+   * The link DOM element.
+   * @type {HTMLAnchorElement}
+   * @private
+   */
+  this.anchor_ = anchor;
+
+  /**
+   * Whether this link represents a link just added to the document.
+   * @type {boolean}
+   * @private
+   */
+  this.isNew_ = isNew;
+
+
+  /**
+   * Any extra anchors created by the browser from a selection in the same
+   * operation that created the primary link
+   * @type {!Array<HTMLAnchorElement>}
+   * @private
+   */
+  this.extraAnchors_ = [];
+};
+
+
+/**
+ * @return {HTMLAnchorElement} The anchor element.
+ */
+goog.editor.Link.prototype.getAnchor = function() {
+  return this.anchor_;
+};
+
+
+/**
+ * @return {!Array<HTMLAnchorElement>} The extra anchor elements, if any,
+ *     created by the browser from a selection.
+ */
+goog.editor.Link.prototype.getExtraAnchors = function() {
+  return this.extraAnchors_;
+};
+
+
+/**
+ * @return {string} The inner text for the anchor.
+ */
+goog.editor.Link.prototype.getCurrentText = function() {
+  if (!this.currentText_) {
+    var anchor = this.getAnchor();
+
+    var leaf = goog.editor.node.getLeftMostLeaf(anchor);
+    if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) {
+      this.currentText_ = leaf.getAttribute('alt');
+    } else {
+      this.currentText_ = goog.dom.getRawTextContent(this.getAnchor());
+    }
+  }
+  return this.currentText_;
+};
+
+
+/**
+ * @return {boolean} Whether the link is new.
+ */
+goog.editor.Link.prototype.isNew = function() {
+  return this.isNew_;
+};
+
+
+/**
+ * Set the url without affecting the isNew() status of the link.
+ * @param {string} url A URL.
+ */
+goog.editor.Link.prototype.initializeUrl = function(url) {
+  this.getAnchor().href = url;
+};
+
+
+/**
+ * Removes the link, leaving its contents in the document.  Note that this
+ * object will no longer be usable/useful after this call.
+ */
+goog.editor.Link.prototype.removeLink = function() {
+  goog.dom.flattenElement(this.anchor_);
+  this.anchor_ = null;
+  while (this.extraAnchors_.length) {
+    goog.dom.flattenElement(/** @type {Element} */(this.extraAnchors_.pop()));
+  }
+};
+
+
+/**
+ * Change the link.
+ * @param {string} newText New text for the link. If the link contains all its
+ *     text in one descendent, newText will only replace the text in that
+ *     one node. Otherwise, we'll change the innerHTML of the whole
+ *     link to newText.
+ * @param {string} newUrl A new URL.
+ */
+goog.editor.Link.prototype.setTextAndUrl = function(newText, newUrl) {
+  var anchor = this.getAnchor();
+  anchor.href = newUrl;
+
+  // If the text did not change, don't update link text.
+  var currentText = this.getCurrentText();
+  if (newText != currentText) {
+    var leaf = goog.editor.node.getLeftMostLeaf(anchor);
+
+    if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) {
+      leaf.setAttribute('alt', newText ? newText : '');
+    } else {
+      if (leaf.nodeType == goog.dom.NodeType.TEXT) {
+        leaf = leaf.parentNode;
+      }
+
+      if (goog.dom.getRawTextContent(leaf) != currentText) {
+        leaf = anchor;
+      }
+
+      goog.dom.removeChildren(leaf);
+      var domHelper = goog.dom.getDomHelper(leaf);
+      goog.dom.appendChild(leaf, domHelper.createTextNode(newText));
+    }
+
+    // The text changed, so force getCurrentText to recompute.
+    this.currentText_ = null;
+  }
+
+  this.isNew_ = false;
+};
+
+
+/**
+ * Places the cursor to the right of the anchor.
+ * Note that this is different from goog.editor.range's placeCursorNextTo
+ * in that it specifically handles the placement of a cursor in browsers
+ * that trap you in links, by adding a space when necessary and placing the
+ * cursor after that space.
+ */
+goog.editor.Link.prototype.placeCursorRightOf = function() {
+  var anchor = this.getAnchor();
+  // If the browser gets stuck in a link if we place the cursor next to it,
+  // we'll place the cursor after a space instead.
+  if (goog.editor.BrowserFeature.GETS_STUCK_IN_LINKS) {
+    var spaceNode;
+    var nextSibling = anchor.nextSibling;
+
+    // Check if there is already a space after the link.  Only handle the
+    // simple case - the next node is a text node that starts with a space.
+    if (nextSibling &&
+        nextSibling.nodeType == goog.dom.NodeType.TEXT &&
+        (goog.string.startsWith(nextSibling.data, goog.string.Unicode.NBSP) ||
+         goog.string.startsWith(nextSibling.data, ' '))) {
+      spaceNode = nextSibling;
+    } else {
+      // If there isn't an obvious space to use, create one after the link.
+      var dh = goog.dom.getDomHelper(anchor);
+      spaceNode = dh.createTextNode(goog.string.Unicode.NBSP);
+      goog.dom.insertSiblingAfter(spaceNode, anchor);
+    }
+
+    // Move the selection after the space.
+    var range = goog.dom.Range.createCaret(spaceNode, 1);
+    range.select();
+  } else {
+    goog.editor.range.placeCursorNextTo(anchor, false);
+  }
+};
+
+
+/**
+ * Updates the cursor position and link bubble for this link.
+ * @param {goog.editor.Field} field The field in which the link is created.
+ * @param {string} url The link url.
+ * @private
+ */
+goog.editor.Link.prototype.updateLinkDisplay_ = function(field, url) {
+  this.initializeUrl(url);
+  this.placeCursorRightOf();
+  field.execCommand(goog.editor.Command.UPDATE_LINK_BUBBLE);
+};
+
+
+/**
+ * @return {string?} The modified string for the link if the link
+ *     text appears to be a valid link. Returns null if this is not
+ *     a valid link address.
+ */
+goog.editor.Link.prototype.getValidLinkFromText = function() {
+  var text = goog.string.trim(this.getCurrentText());
+  if (goog.editor.Link.isLikelyUrl(text)) {
+    if (text.search(/:/) < 0) {
+      return 'http://' + goog.string.trimLeft(text);
+    }
+    return text;
+  } else if (goog.editor.Link.isLikelyEmailAddress(text)) {
+    return 'mailto:' + text;
+  }
+  return null;
+};
+
+
+/**
+ * After link creation, finish creating the link depending on the type
+ * of link being created.
+ * @param {goog.editor.Field} field The field where this link is being created.
+ */
+goog.editor.Link.prototype.finishLinkCreation = function(field) {
+  var linkFromText = this.getValidLinkFromText();
+  if (linkFromText) {
+    this.updateLinkDisplay_(field, linkFromText);
+  } else {
+    field.execCommand(goog.editor.Command.MODAL_LINK_EDITOR, this);
+  }
+};
+
+
+/**
+ * Initialize a new link.
+ * @param {HTMLAnchorElement} anchor The anchor element.
+ * @param {string} url The initial URL.
+ * @param {string=} opt_target The target.
+ * @param {Array<HTMLAnchorElement>=} opt_extraAnchors Extra anchors created
+ *     by the browser when parsing a selection.
+ * @return {!goog.editor.Link} The link.
+ */
+goog.editor.Link.createNewLink = function(anchor, url, opt_target,
+    opt_extraAnchors) {
+  var link = new goog.editor.Link(anchor, true);
+  link.initializeUrl(url);
+
+  if (opt_target) {
+    anchor.target = opt_target;
+  }
+  if (opt_extraAnchors) {
+    link.extraAnchors_ = opt_extraAnchors;
+  }
+
+  return link;
+};
+
+
+/**
+ * Initialize a new link using text in anchor, or empty string if there is no
+ * likely url in the anchor.
+ * @param {HTMLAnchorElement} anchor The anchor element with likely url 
content.
+ * @param {string=} opt_target The target.
+ * @return {!goog.editor.Link} The link.
+ */
+goog.editor.Link.createNewLinkFromText = function(anchor, opt_target) {
+  var link = new goog.editor.Link(anchor, true);
+  var text = link.getValidLinkFromText();
+  link.initializeUrl(text ? text : '');
+  if (opt_target) {
+    anchor.target = opt_target;
+  }
+  return link;
+};
+
+
+/**
+ * Returns true if str could be a URL, false otherwise
+ *
+ * Ex: TR_Util.isLikelyUrl_("http://www.google.com";) == true
+ *     TR_Util.isLikelyUrl_("www.google.com") == true
+ *
+ * @param {string} str String to check if it looks like a URL.
+ * @return {boolean} Whether str could be a URL.
+ */
+goog.editor.Link.isLikelyUrl = function(str) {
+  // Whitespace means this isn't a domain.
+  if (/\s/.test(str)) {
+    return false;
+  }
+
+  if (goog.editor.Link.isLikelyEmailAddress(str)) {
+    return false;
+  }
+
+  // Add a scheme if the url doesn't have one - this helps the parser.
+  var addedScheme = false;
+  if (!/^[^:\/?#.]+:/.test(str)) {
+    str = 'http://' + str;
+    addedScheme = true;
+  }
+
+  // Parse the domain.
+  var parts = goog.uri.utils.split(str);
+
+  // Relax the rules for special schemes.
+  var scheme = parts[goog.uri.utils.ComponentIndex.SCHEME];
+  if (goog.array.indexOf(['mailto', 'aim'], scheme) != -1) {
+    return true;
+  }
+
+  // Require domains to contain a '.', unless the domain is fully qualified and
+  // forbids domains from containing invalid characters.
+  var domain = parts[goog.uri.utils.ComponentIndex.DOMAIN];
+  if (!domain || (addedScheme && domain.indexOf('.') == -1) ||
+      (/[^\w\d\-\u0100-\uffff.%]/.test(domain))) {
+    return false;
+  }
+
+  // Require http and ftp paths to start with '/'.
+  var path = parts[goog.uri.utils.ComponentIndex.PATH];
+  return !path || path.indexOf('/') == 0;
+};
+
+
+/**
+ * Regular expression that matches strings that could be an email address.
+ * @type {RegExp}
+ * @private
+ */
+goog.editor.Link.LIKELY_EMAIL_ADDRESS_ = new RegExp(
+    '^' +                     // Test from start of string
+    '[\\w-]+(\\.[\\w-]+)*' +  // Dot-delimited alphanumerics and dashes (name)
+    '\\@' +                   // @
+    '([\\w-]+\\.)+' +         // Alphanumerics, dashes and dots (domain)
+    '(\\d+|\\w\\w+)$',        // Domain ends in at least one number or 2 
letters
+    'i');
+
+
+/**
+ * Returns true if str could be an email address, false otherwise
+ *
+ * Ex: goog.editor.Link.isLikelyEmailAddress_("some word") == false
+ *     goog.editor.Link.isLikelyEmailAddress_("[email protected]") == true
+ *
+ * @param {string} str String to test for being email address.
+ * @return {boolean} Whether "str" looks like an email address.
+ */
+goog.editor.Link.isLikelyEmailAddress = function(str) {
+  return goog.editor.Link.LIKELY_EMAIL_ADDRESS_.test(str);
+};
+
+
+/**
+ * Determines whether or not a url is an email link.
+ * @param {string} url A url.
+ * @return {boolean} Whether the url is a mailto link.
+ */
+goog.editor.Link.isMailto = function(url) {
+  return !!url && goog.string.startsWith(url, 'mailto:');
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/node.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/node.js 
b/externs/GCL/externs/goog/editor/node.js
new file mode 100644
index 0000000..006936d
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/node.js
@@ -0,0 +1,484 @@
+// Copyright 2005 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilties for working with DOM nodes related to rich text
+ * editing.  Many of these are not general enough to go into goog.dom.
+ *
+ * @author [email protected] (Nick Santos)
+ */
+
+goog.provide('goog.editor.node');
+
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.TagName');
+goog.require('goog.dom.iter.ChildIterator');
+goog.require('goog.dom.iter.SiblingIterator');
+goog.require('goog.iter');
+goog.require('goog.object');
+goog.require('goog.string');
+goog.require('goog.string.Unicode');
+goog.require('goog.userAgent');
+
+
+/**
+ * Names of all block-level tags
+ * @type {Object}
+ * @private
+ */
+goog.editor.node.BLOCK_TAG_NAMES_ = goog.object.createSet(
+    goog.dom.TagName.ADDRESS,
+    goog.dom.TagName.ARTICLE,
+    goog.dom.TagName.ASIDE,
+    goog.dom.TagName.BLOCKQUOTE,
+    goog.dom.TagName.BODY,
+    goog.dom.TagName.CAPTION,
+    goog.dom.TagName.CENTER,
+    goog.dom.TagName.COL,
+    goog.dom.TagName.COLGROUP,
+    goog.dom.TagName.DETAILS,
+    goog.dom.TagName.DIR,
+    goog.dom.TagName.DIV,
+    goog.dom.TagName.DL,
+    goog.dom.TagName.DD,
+    goog.dom.TagName.DT,
+    goog.dom.TagName.FIELDSET,
+    goog.dom.TagName.FIGCAPTION,
+    goog.dom.TagName.FIGURE,
+    goog.dom.TagName.FOOTER,
+    goog.dom.TagName.FORM,
+    goog.dom.TagName.H1,
+    goog.dom.TagName.H2,
+    goog.dom.TagName.H3,
+    goog.dom.TagName.H4,
+    goog.dom.TagName.H5,
+    goog.dom.TagName.H6,
+    goog.dom.TagName.HEADER,
+    goog.dom.TagName.HGROUP,
+    goog.dom.TagName.HR,
+    goog.dom.TagName.ISINDEX,
+    goog.dom.TagName.OL,
+    goog.dom.TagName.LI,
+    goog.dom.TagName.MAP,
+    goog.dom.TagName.MENU,
+    goog.dom.TagName.NAV,
+    goog.dom.TagName.OPTGROUP,
+    goog.dom.TagName.OPTION,
+    goog.dom.TagName.P,
+    goog.dom.TagName.PRE,
+    goog.dom.TagName.SECTION,
+    goog.dom.TagName.SUMMARY,
+    goog.dom.TagName.TABLE,
+    goog.dom.TagName.TBODY,
+    goog.dom.TagName.TD,
+    goog.dom.TagName.TFOOT,
+    goog.dom.TagName.TH,
+    goog.dom.TagName.THEAD,
+    goog.dom.TagName.TR,
+    goog.dom.TagName.UL);
+
+
+/**
+ * Names of tags that have intrinsic content.
+ * TODO(robbyw): What about object, br, input, textarea, button, isindex,
+ * hr, keygen, select, table, tr, td?
+ * @type {Object}
+ * @private
+ */
+goog.editor.node.NON_EMPTY_TAGS_ = goog.object.createSet(
+    goog.dom.TagName.IMG, goog.dom.TagName.IFRAME, goog.dom.TagName.EMBED);
+
+
+/**
+ * Check if the node is in a standards mode document.
+ * @param {Node} node The node to test.
+ * @return {boolean} Whether the node is in a standards mode document.
+ */
+goog.editor.node.isStandardsMode = function(node) {
+  return goog.dom.getDomHelper(node).isCss1CompatMode();
+};
+
+
+/**
+ * Get the right-most non-ignorable leaf node of the given node.
+ * @param {Node} parent The parent ndoe.
+ * @return {Node} The right-most non-ignorable leaf node.
+ */
+goog.editor.node.getRightMostLeaf = function(parent) {
+  var temp;
+  while (temp = goog.editor.node.getLastChild(parent)) {
+    parent = temp;
+  }
+  return parent;
+};
+
+
+/**
+ * Get the left-most non-ignorable leaf node of the given node.
+ * @param {Node} parent The parent ndoe.
+ * @return {Node} The left-most non-ignorable leaf node.
+ */
+goog.editor.node.getLeftMostLeaf = function(parent) {
+  var temp;
+  while (temp = goog.editor.node.getFirstChild(parent)) {
+    parent = temp;
+  }
+  return parent;
+};
+
+
+/**
+ * Version of firstChild that skips nodes that are entirely
+ * whitespace and comments.
+ * @param {Node} parent The reference node.
+ * @return {Node} The first child of sibling that is important according to
+ *     goog.editor.node.isImportant, or null if no such node exists.
+ */
+goog.editor.node.getFirstChild = function(parent) {
+  return goog.editor.node.getChildHelper_(parent, false);
+};
+
+
+/**
+ * Version of lastChild that skips nodes that are entirely whitespace or
+ * comments.  (Normally lastChild is a property of all DOM nodes that gives the
+ * last of the nodes contained directly in the reference node.)
+ * @param {Node} parent The reference node.
+ * @return {Node} The last child of sibling that is important according to
+ *     goog.editor.node.isImportant, or null if no such node exists.
+ */
+goog.editor.node.getLastChild = function(parent) {
+  return goog.editor.node.getChildHelper_(parent, true);
+};
+
+
+/**
+ * Version of previoussibling that skips nodes that are entirely
+ * whitespace or comments.  (Normally previousSibling is a property
+ * of all DOM nodes that gives the sibling node, the node that is
+ * a child of the same parent, that occurs immediately before the
+ * reference node.)
+ * @param {Node} sibling The reference node.
+ * @return {Node} The closest previous sibling to sibling that is
+ *     important according to goog.editor.node.isImportant, or null if no such
+ *     node exists.
+ */
+goog.editor.node.getPreviousSibling = function(sibling) {
+  return /** @type {Node} */ (goog.editor.node.getFirstValue_(
+      goog.iter.filter(new goog.dom.iter.SiblingIterator(sibling, false, true),
+      goog.editor.node.isImportant)));
+};
+
+
+/**
+ * Version of nextSibling that skips nodes that are entirely whitespace or
+ * comments.
+ * @param {Node} sibling The reference node.
+ * @return {Node} The closest next sibling to sibling that is important
+ *     according to goog.editor.node.isImportant, or null if no
+ *     such node exists.
+ */
+goog.editor.node.getNextSibling = function(sibling) {
+  return /** @type {Node} */ (goog.editor.node.getFirstValue_(
+      goog.iter.filter(new goog.dom.iter.SiblingIterator(sibling),
+      goog.editor.node.isImportant)));
+};
+
+
+/**
+ * Internal helper for lastChild/firstChild that skips nodes that are entirely
+ * whitespace or comments.
+ * @param {Node} parent The reference node.
+ * @param {boolean} isReversed Whether children should be traversed forward
+ *     or backward.
+ * @return {Node} The first/last child of sibling that is important according
+ *     to goog.editor.node.isImportant, or null if no such node exists.
+ * @private
+ */
+goog.editor.node.getChildHelper_ = function(parent, isReversed) {
+  return (!parent || parent.nodeType != goog.dom.NodeType.ELEMENT) ? null :
+      /** @type {Node} */ (goog.editor.node.getFirstValue_(goog.iter.filter(
+          new goog.dom.iter.ChildIterator(
+              /** @type {!Element} */ (parent), isReversed),
+          goog.editor.node.isImportant)));
+};
+
+
+/**
+ * Utility function that returns the first value from an iterator or null if
+ * the iterator is empty.
+ * @param {goog.iter.Iterator} iterator The iterator to get a value from.
+ * @return {*} The first value from the iterator.
+ * @private
+ */
+goog.editor.node.getFirstValue_ = function(iterator) {
+  /** @preserveTry */
+  try {
+    return iterator.next();
+  } catch (e) {
+    return null;
+  }
+};
+
+
+/**
+ * Determine if a node should be returned by the iterator functions.
+ * @param {Node} node An object implementing the DOM1 Node interface.
+ * @return {boolean} Whether the node is an element, or a text node that
+ *     is not all whitespace.
+ */
+goog.editor.node.isImportant = function(node) {
+  // Return true if the node is not either a TextNode or an ElementNode.
+  return node.nodeType == goog.dom.NodeType.ELEMENT ||
+         node.nodeType == goog.dom.NodeType.TEXT &&
+         !goog.editor.node.isAllNonNbspWhiteSpace(node);
+};
+
+
+/**
+ * Determine whether a node's text content is entirely whitespace.
+ * @param {Node} textNode A node implementing the CharacterData interface 
(i.e.,
+ *     a Text, Comment, or CDATASection node.
+ * @return {boolean} Whether the text content of node is whitespace,
+ *     otherwise false.
+ */
+goog.editor.node.isAllNonNbspWhiteSpace = function(textNode) {
+  return goog.string.isBreakingWhitespace(textNode.nodeValue);
+};
+
+
+/**
+ * Returns true if the node contains only whitespace and is not and does not
+ * contain any images, iframes or embed tags.
+ * @param {Node} node The node to check.
+ * @param {boolean=} opt_prohibitSingleNbsp By default, this function treats a
+ *     single nbsp as empty.  Set this to true to treat this case as non-empty.
+ * @return {boolean} Whether the node contains only whitespace.
+ */
+goog.editor.node.isEmpty = function(node, opt_prohibitSingleNbsp) {
+  var nodeData = goog.dom.getRawTextContent(node);
+
+  if (node.getElementsByTagName) {
+    for (var tag in goog.editor.node.NON_EMPTY_TAGS_) {
+      if (node.tagName == tag || node.getElementsByTagName(tag).length > 0) {
+        return false;
+      }
+    }
+  }
+  return (!opt_prohibitSingleNbsp && nodeData == goog.string.Unicode.NBSP) ||
+      goog.string.isBreakingWhitespace(nodeData);
+};
+
+
+/**
+ * Returns the length of the text in node if it is a text node, or the number
+ * of children of the node, if it is an element. Useful for range-manipulation
+ * code where you need to know the offset for the right side of the node.
+ * @param {Node} node The node to get the length of.
+ * @return {number} The length of the node.
+ */
+goog.editor.node.getLength = function(node) {
+  return node.length || node.childNodes.length;
+};
+
+
+/**
+ * Search child nodes using a predicate function and return the first node that
+ * satisfies the condition.
+ * @param {Node} parent The parent node to search.
+ * @param {function(Node):boolean} hasProperty A function that takes a child
+ *    node as a parameter and returns true if it meets the criteria.
+ * @return {?number} The index of the node found, or null if no node is found.
+ */
+goog.editor.node.findInChildren = function(parent, hasProperty) {
+  for (var i = 0, len = parent.childNodes.length; i < len; i++) {
+    if (hasProperty(parent.childNodes[i])) {
+      return i;
+    }
+  }
+  return null;
+};
+
+
+/**
+ * Search ancestor nodes using a predicate function and returns the topmost
+ * ancestor in the chain of consecutive ancestors that satisfies the condition.
+ *
+ * @param {Node} node The node whose ancestors have to be searched.
+ * @param {function(Node): boolean} hasProperty A function that takes a parent
+ *     node as a parameter and returns true if it meets the criteria.
+ * @return {Node} The topmost ancestor or null if no ancestor satisfies the
+ *     predicate function.
+ */
+goog.editor.node.findHighestMatchingAncestor = function(node, hasProperty) {
+  var parent = node.parentNode;
+  var ancestor = null;
+  while (parent && hasProperty(parent)) {
+    ancestor = parent;
+    parent = parent.parentNode;
+  }
+  return ancestor;
+};
+
+
+/**
+* Checks if node is a block-level html element. The <tt>display</tt> css
+ * property is ignored.
+ * @param {Node} node The node to test.
+ * @return {boolean} Whether the node is a block-level node.
+ */
+goog.editor.node.isBlockTag = function(node) {
+  return !!goog.editor.node.BLOCK_TAG_NAMES_[node.tagName];
+};
+
+
+/**
+ * Skips siblings of a node that are empty text nodes.
+ * @param {Node} node A node. May be null.
+ * @return {Node} The node or the first sibling of the node that is not an
+ *     empty text node. May be null.
+ */
+goog.editor.node.skipEmptyTextNodes = function(node) {
+  while (node && node.nodeType == goog.dom.NodeType.TEXT &&
+      !node.nodeValue) {
+    node = node.nextSibling;
+  }
+  return node;
+};
+
+
+/**
+ * Checks if an element is a top-level editable container (meaning that
+ * it itself is not editable, but all its child nodes are editable).
+ * @param {Node} element The element to test.
+ * @return {boolean} Whether the element is a top-level editable container.
+ */
+goog.editor.node.isEditableContainer = function(element) {
+  return element.getAttribute &&
+      element.getAttribute('g_editable') == 'true';
+};
+
+
+/**
+ * Checks if a node is inside an editable container.
+ * @param {Node} node The node to test.
+ * @return {boolean} Whether the node is in an editable container.
+ */
+goog.editor.node.isEditable = function(node) {
+  return !!goog.dom.getAncestor(node, goog.editor.node.isEditableContainer);
+};
+
+
+/**
+ * Finds the top-most DOM node inside an editable field that is an ancestor
+ * (or self) of a given DOM node and meets the specified criteria.
+ * @param {Node} node The DOM node where the search starts.
+ * @param {function(Node) : boolean} criteria A function that takes a DOM node
+ *     as a parameter and returns a boolean to indicate whether the node meets
+ *     the criteria or not.
+ * @return {Node} The DOM node if found, or null.
+ */
+goog.editor.node.findTopMostEditableAncestor = function(node, criteria) {
+  var targetNode = null;
+  while (node && !goog.editor.node.isEditableContainer(node)) {
+    if (criteria(node)) {
+      targetNode = node;
+    }
+    node = node.parentNode;
+  }
+  return targetNode;
+};
+
+
+/**
+ * Splits off a subtree.
+ * @param {!Node} currentNode The starting splitting point.
+ * @param {Node=} opt_secondHalf The initial leftmost leaf the new subtree.
+ *     If null, siblings after currentNode will be placed in the subtree, but
+ *     no additional node will be.
+ * @param {Node=} opt_root The top of the tree where splitting stops at.
+ * @return {!Node} The new subtree.
+ */
+goog.editor.node.splitDomTreeAt = function(currentNode,
+    opt_secondHalf, opt_root) {
+  var parent;
+  while (currentNode != opt_root && (parent = currentNode.parentNode)) {
+    opt_secondHalf = goog.editor.node.getSecondHalfOfNode_(parent, currentNode,
+        opt_secondHalf);
+    currentNode = parent;
+  }
+  return /** @type {!Node} */(opt_secondHalf);
+};
+
+
+/**
+ * Creates a clone of node, moving all children after startNode to it.
+ * When firstChild is not null or undefined, it is also appended to the clone
+ * as the first child.
+ * @param {!Node} node The node to clone.
+ * @param {!Node} startNode All siblings after this node will be moved to the
+ *     clone.
+ * @param {Node|undefined} firstChild The first child of the new cloned 
element.
+ * @return {!Node} The cloned node that now contains the children after
+ *     startNode.
+ * @private
+ */
+goog.editor.node.getSecondHalfOfNode_ = function(node, startNode, firstChild) {
+  var secondHalf = /** @type {!Node} */(node.cloneNode(false));
+  while (startNode.nextSibling) {
+    goog.dom.appendChild(secondHalf, startNode.nextSibling);
+  }
+  if (firstChild) {
+    secondHalf.insertBefore(firstChild, secondHalf.firstChild);
+  }
+  return secondHalf;
+};
+
+
+/**
+ * Appends all of oldNode's children to newNode. This removes all children from
+ * oldNode and appends them to newNode. oldNode is left with no children.
+ * @param {!Node} newNode Node to transfer children to.
+ * @param {Node} oldNode Node to transfer children from.
+ * @deprecated Use goog.dom.append directly instead.
+ */
+goog.editor.node.transferChildren = function(newNode, oldNode) {
+  goog.dom.append(newNode, oldNode.childNodes);
+};
+
+
+/**
+ * Replaces the innerHTML of a node.
+ *
+ * IE has serious problems if you try to set innerHTML of an editable node with
+ * any selection. Early versions of IE tear up the old internal tree storage, 
to
+ * help avoid ref-counting loops. But this sometimes leaves the selection 
object
+ * in a bad state and leads to segfaults.
+ *
+ * Removing the nodes first prevents IE from tearing them up. This is not
+ * strictly necessary in nodes that do not have the selection. You should 
always
+ * use this function when setting innerHTML inside of a field.
+ *
+ * @param {Node} node A node.
+ * @param {string} html The innerHTML to set on the node.
+ */
+goog.editor.node.replaceInnerHtml = function(node, html) {
+  // Only do this IE. On gecko, we use element change events, and don't
+  // want to trigger spurious events.
+  if (goog.userAgent.IE) {
+    goog.dom.removeChildren(node);
+  }
+  node.innerHTML = html;
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugin.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugin.js 
b/externs/GCL/externs/goog/editor/plugin.js
new file mode 100644
index 0000000..d823ccb
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugin.js
@@ -0,0 +1,463 @@
+// Copyright 2008 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// All Rights Reserved.
+
+/**
+ * @fileoverview Abstract API for TrogEdit plugins.
+ *
+ * @see ../demos/editor/editor.html
+ */
+
+goog.provide('goog.editor.Plugin');
+
+// TODO(user): Remove the dependency on goog.editor.Command asap. Currently 
only
+// needed for execCommand issues with links.
+goog.require('goog.events.EventTarget');
+goog.require('goog.functions');
+goog.require('goog.log');
+goog.require('goog.object');
+goog.require('goog.reflect');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * Abstract API for trogedit plugins.
+ * @constructor
+ * @extends {goog.events.EventTarget}
+ */
+goog.editor.Plugin = function() {
+  goog.events.EventTarget.call(this);
+
+  /**
+   * Whether this plugin is enabled for the registered field object.
+   * @type {boolean}
+   * @private
+   */
+  this.enabled_ = this.activeOnUneditableFields();
+};
+goog.inherits(goog.editor.Plugin, goog.events.EventTarget);
+
+
+/**
+ * The field object this plugin is attached to.
+ * @type {goog.editor.Field}
+ * @protected
+ * @deprecated Use goog.editor.Plugin.getFieldObject and
+ *     goog.editor.Plugin.setFieldObject.
+ */
+goog.editor.Plugin.prototype.fieldObject = null;
+
+
+/**
+ * @return {goog.dom.DomHelper?} The dom helper object associated with the
+ *     currently active field.
+ */
+goog.editor.Plugin.prototype.getFieldDomHelper = function() {
+  return this.getFieldObject() && this.getFieldObject().getEditableDomHelper();
+};
+
+
+/**
+ * Indicates if this plugin should be automatically disposed when the
+ * registered field is disposed. This should be changed to false for
+ * plugins used as multi-field plugins.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.Plugin.prototype.autoDispose_ = true;
+
+
+/**
+ * The logger for this plugin.
+ * @type {goog.log.Logger}
+ * @protected
+ */
+goog.editor.Plugin.prototype.logger =
+    goog.log.getLogger('goog.editor.Plugin');
+
+
+/**
+ * Sets the field object for use with this plugin.
+ * @return {goog.editor.Field} The editable field object.
+ * @protected
+ * @suppress {deprecated} Until fieldObject can be made private.
+ */
+goog.editor.Plugin.prototype.getFieldObject = function() {
+  return this.fieldObject;
+};
+
+
+/**
+ * Sets the field object for use with this plugin.
+ * @param {goog.editor.Field} fieldObject The editable field object.
+ * @protected
+ * @suppress {deprecated} Until fieldObject can be made private.
+ */
+goog.editor.Plugin.prototype.setFieldObject = function(fieldObject) {
+  this.fieldObject = fieldObject;
+};
+
+
+/**
+ * Registers the field object for use with this plugin.
+ * @param {goog.editor.Field} fieldObject The editable field object.
+ */
+goog.editor.Plugin.prototype.registerFieldObject = function(fieldObject) {
+  this.setFieldObject(fieldObject);
+};
+
+
+/**
+ * Unregisters and disables this plugin for the current field object.
+ * @param {goog.editor.Field} fieldObj The field object. For single-field
+ *     plugins, this parameter is ignored.
+ */
+goog.editor.Plugin.prototype.unregisterFieldObject = function(fieldObj) {
+  if (this.getFieldObject()) {
+    this.disable(this.getFieldObject());
+    this.setFieldObject(null);
+  }
+};
+
+
+/**
+ * Enables this plugin for the specified, registered field object. A field
+ * object should only be enabled when it is loaded.
+ * @param {goog.editor.Field} fieldObject The field object.
+ */
+goog.editor.Plugin.prototype.enable = function(fieldObject) {
+  if (this.getFieldObject() == fieldObject) {
+    this.enabled_ = true;
+  } else {
+    goog.log.error(this.logger, 'Trying to enable an unregistered field with ' 
+
+        'this plugin.');
+  }
+};
+
+
+/**
+ * Disables this plugin for the specified, registered field object.
+ * @param {goog.editor.Field} fieldObject The field object.
+ */
+goog.editor.Plugin.prototype.disable = function(fieldObject) {
+  if (this.getFieldObject() == fieldObject) {
+    this.enabled_ = false;
+  } else {
+    goog.log.error(this.logger, 'Trying to disable an unregistered field ' +
+        'with this plugin.');
+  }
+};
+
+
+/**
+ * Returns whether this plugin is enabled for the field object.
+ *
+ * @param {goog.editor.Field} fieldObject The field object.
+ * @return {boolean} Whether this plugin is enabled for the field object.
+ */
+goog.editor.Plugin.prototype.isEnabled = function(fieldObject) {
+  return this.getFieldObject() == fieldObject ? this.enabled_ : false;
+};
+
+
+/**
+ * Set if this plugin should automatically be disposed when the registered
+ * field is disposed.
+ * @param {boolean} autoDispose Whether to autoDispose.
+ */
+goog.editor.Plugin.prototype.setAutoDispose = function(autoDispose) {
+  this.autoDispose_ = autoDispose;
+};
+
+
+/**
+ * @return {boolean} Whether or not this plugin should automatically be 
disposed
+ *     when it's registered field is disposed.
+ */
+goog.editor.Plugin.prototype.isAutoDispose = function() {
+  return this.autoDispose_;
+};
+
+
+/**
+ * @return {boolean} If true, field will not disable the command
+ *     when the field becomes uneditable.
+ */
+goog.editor.Plugin.prototype.activeOnUneditableFields = goog.functions.FALSE;
+
+
+/**
+ * @param {string} command The command to check.
+ * @return {boolean} If true, field will not dispatch change events
+ *     for commands of this type. This is useful for "seamless" plugins like
+ *     dialogs and lorem ipsum.
+ */
+goog.editor.Plugin.prototype.isSilentCommand = goog.functions.FALSE;
+
+
+/** @override */
+goog.editor.Plugin.prototype.disposeInternal = function() {
+  if (this.getFieldObject()) {
+    this.unregisterFieldObject(this.getFieldObject());
+  }
+
+  goog.editor.Plugin.superClass_.disposeInternal.call(this);
+};
+
+
+/**
+ * @return {string} The ID unique to this plugin class. Note that different
+ *     instances off the plugin share the same classId.
+ */
+goog.editor.Plugin.prototype.getTrogClassId;
+
+
+/**
+ * An enum of operations that plugins may support.
+ * @enum {number}
+ */
+goog.editor.Plugin.Op = {
+  KEYDOWN: 1,
+  KEYPRESS: 2,
+  KEYUP: 3,
+  SELECTION: 4,
+  SHORTCUT: 5,
+  EXEC_COMMAND: 6,
+  QUERY_COMMAND: 7,
+  PREPARE_CONTENTS_HTML: 8,
+  CLEAN_CONTENTS_HTML: 10,
+  CLEAN_CONTENTS_DOM: 11
+};
+
+
+/**
+ * A map from plugin operations to the names of the methods that
+ * invoke those operations.
+ */
+goog.editor.Plugin.OPCODE = goog.object.transpose(
+    goog.reflect.object(goog.editor.Plugin, {
+      handleKeyDown: goog.editor.Plugin.Op.KEYDOWN,
+      handleKeyPress: goog.editor.Plugin.Op.KEYPRESS,
+      handleKeyUp: goog.editor.Plugin.Op.KEYUP,
+      handleSelectionChange: goog.editor.Plugin.Op.SELECTION,
+      handleKeyboardShortcut: goog.editor.Plugin.Op.SHORTCUT,
+      execCommand: goog.editor.Plugin.Op.EXEC_COMMAND,
+      queryCommandValue: goog.editor.Plugin.Op.QUERY_COMMAND,
+      prepareContentsHtml: goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML,
+      cleanContentsHtml: goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML,
+      cleanContentsDom: goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM
+    }));
+
+
+/**
+ * A set of op codes that run even on disabled plugins.
+ */
+goog.editor.Plugin.IRREPRESSIBLE_OPS = goog.object.createSet(
+    goog.editor.Plugin.Op.PREPARE_CONTENTS_HTML,
+    goog.editor.Plugin.Op.CLEAN_CONTENTS_HTML,
+    goog.editor.Plugin.Op.CLEAN_CONTENTS_DOM);
+
+
+/**
+ * Handles keydown. It is run before handleKeyboardShortcut and if it returns
+ * true handleKeyboardShortcut will not be called.
+ * @param {!goog.events.BrowserEvent} e The browser event.
+ * @return {boolean} Whether the event was handled and thus should *not* be
+ *     propagated to other plugins or handleKeyboardShortcut.
+ */
+goog.editor.Plugin.prototype.handleKeyDown;
+
+
+/**
+ * Handles keypress. It is run before handleKeyboardShortcut and if it returns
+ * true handleKeyboardShortcut will not be called.
+ * @param {!goog.events.BrowserEvent} e The browser event.
+ * @return {boolean} Whether the event was handled and thus should *not* be
+ *     propagated to other plugins or handleKeyboardShortcut.
+ */
+goog.editor.Plugin.prototype.handleKeyPress;
+
+
+/**
+ * Handles keyup.
+ * @param {!goog.events.BrowserEvent} e The browser event.
+ * @return {boolean} Whether the event was handled and thus should *not* be
+ *     propagated to other plugins.
+ */
+goog.editor.Plugin.prototype.handleKeyUp;
+
+
+/**
+ * Handles selection change.
+ * @param {!goog.events.BrowserEvent=} opt_e The browser event.
+ * @param {!Node=} opt_target The node the selection changed to.
+ * @return {boolean} Whether the event was handled and thus should *not* be
+ *     propagated to other plugins.
+ */
+goog.editor.Plugin.prototype.handleSelectionChange;
+
+
+/**
+ * Handles keyboard shortcuts.  Preferred to using handleKey* as it will use
+ * the proper event based on browser and will be more performant. If
+ * handleKeyPress/handleKeyDown returns true, this will not be called. If the
+ * plugin handles the shortcut, it is responsible for dispatching appropriate
+ * events (change, selection change at the time of this comment). If the plugin
+ * calls execCommand on the editable field, then execCommand already takes care
+ * of dispatching events.
+ * NOTE: For performance reasons this is only called when any key is pressed
+ * in conjunction with ctrl/meta keys OR when a small subset of keys (defined
+ * in goog.editor.Field.POTENTIAL_SHORTCUT_KEYCODES_) are pressed without
+ * ctrl/meta keys. We specifically don't invoke it when altKey is pressed since
+ * alt key is used in many i8n UIs to enter certain characters.
+ * @param {!goog.events.BrowserEvent} e The browser event.
+ * @param {string} key The key pressed.
+ * @param {boolean} isModifierPressed Whether the ctrl/meta key was pressed or
+ *     not.
+ * @return {boolean} Whether the event was handled and thus should *not* be
+ *     propagated to other plugins. We also call preventDefault on the event if
+ *     the return value is true.
+ */
+goog.editor.Plugin.prototype.handleKeyboardShortcut;
+
+
+/**
+ * Handles execCommand. This default implementation handles dispatching
+ * BEFORECHANGE, CHANGE, and SELECTIONCHANGE events, and calls
+ * execCommandInternal to perform the actual command. Plugins that want to
+ * do their own event dispatching should override execCommand, otherwise
+ * it is preferred to only override execCommandInternal.
+ *
+ * This version of execCommand will only work for single field plugins.
+ * Multi-field plugins must override execCommand.
+ *
+ * @param {string} command The command to execute.
+ * @param {...*} var_args Any additional parameters needed to
+ *     execute the command.
+ * @return {*} The result of the execCommand, if any.
+ */
+goog.editor.Plugin.prototype.execCommand = function(command, var_args) {
+  // TODO(user): Replace all uses of isSilentCommand with plugins that just
+  // override this base execCommand method.
+  var silent = this.isSilentCommand(command);
+  if (!silent) {
+    // Stop listening to mutation events in Firefox while text formatting
+    // is happening.  This prevents us from trying to size the field in the
+    // middle of an execCommand, catching the field in a strange intermediary
+    // state where both replacement nodes and original nodes are appended to
+    // the dom.  Note that change events get turned back on by
+    // fieldObj.dispatchChange.
+    if (goog.userAgent.GECKO) {
+      this.getFieldObject().stopChangeEvents(true, true);
+    }
+
+    this.getFieldObject().dispatchBeforeChange();
+  }
+
+  try {
+    var result = this.execCommandInternal.apply(this, arguments);
+  } finally {
+    // If the above execCommandInternal call throws an exception, we still need
+    // to turn change events back on (see http://b/issue?id=1471355).
+    // NOTE: If if you add to or change the methods called in this finally
+    // block, please add them as expected calls to the unit test function
+    // testExecCommandException().
+    if (!silent) {
+      // dispatchChange includes a call to startChangeEvents, which unwinds the
+      // call to stopChangeEvents made before the try block.
+      this.getFieldObject().dispatchChange();
+      this.getFieldObject().dispatchSelectionChangeEvent();
+    }
+  }
+
+  return result;
+};
+
+
+/**
+ * Handles execCommand. This default implementation does nothing, and is
+ * called by execCommand, which handles event dispatching. This method should
+ * be overriden by plugins that don't need to do their own event dispatching.
+ * If custom event dispatching is needed, execCommand shoul be overriden
+ * instead.
+ *
+ * @param {string} command The command to execute.
+ * @param {...*} var_args Any additional parameters needed to
+ *     execute the command.
+ * @return {*} The result of the execCommand, if any.
+ * @protected
+ */
+goog.editor.Plugin.prototype.execCommandInternal;
+
+
+/**
+ * Gets the state of this command if this plugin serves that command.
+ * @param {string} command The command to check.
+ * @return {*} The value of the command.
+ */
+goog.editor.Plugin.prototype.queryCommandValue;
+
+
+/**
+ * Prepares the given HTML for editing. Strips out content that should not
+ * appear in an editor, and normalizes content as appropriate. The inverse
+ * of cleanContentsHtml.
+ *
+ * This op is invoked even on disabled plugins.
+ *
+ * @param {string} originalHtml The original HTML.
+ * @param {Object} styles A map of strings. If the plugin wants to add
+ *     any styles to the field element, it should add them as key-value
+ *     pairs to this object.
+ * @return {string} New HTML that's ok for editing.
+ */
+goog.editor.Plugin.prototype.prepareContentsHtml;
+
+
+/**
+ * Cleans the contents of the node passed to it. The node contents are modified
+ * directly, and the modifications will subsequently be used, for operations
+ * such as saving the innerHTML of the editor etc. Since the plugins act on
+ * the DOM directly, this method can be very expensive.
+ *
+ * This op is invoked even on disabled plugins.
+ *
+ * @param {!Element} fieldCopy The copy of the editable field which
+ *     needs to be cleaned up.
+ */
+goog.editor.Plugin.prototype.cleanContentsDom;
+
+
+/**
+ * Cleans the html contents of Trogedit. Both cleanContentsDom and
+ * and cleanContentsHtml will be called on contents extracted from Trogedit.
+ * The inverse of prepareContentsHtml.
+ *
+ * This op is invoked even on disabled plugins.
+ *
+ * @param {string} originalHtml The trogedit HTML.
+ * @return {string} Cleaned-up HTML.
+ */
+goog.editor.Plugin.prototype.cleanContentsHtml;
+
+
+/**
+ * Whether the string corresponds to a command this plugin handles.
+ * @param {string} command Command string to check.
+ * @return {boolean} Whether the plugin handles this type of command.
+ */
+goog.editor.Plugin.prototype.isSupportedCommand = function(command) {
+  return false;
+};

http://git-wip-us.apache.org/repos/asf/flex-falcon/blob/e2cad6e6/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js
----------------------------------------------------------------------
diff --git a/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js 
b/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js
new file mode 100644
index 0000000..4951ed6
--- /dev/null
+++ b/externs/GCL/externs/goog/editor/plugins/abstractbubbleplugin.js
@@ -0,0 +1,712 @@
+// Copyright 2005 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Base class for bubble plugins.
+ * @author [email protected] (Robby Walker)
+ */
+
+goog.provide('goog.editor.plugins.AbstractBubblePlugin');
+
+goog.require('goog.array');
+goog.require('goog.dom');
+goog.require('goog.dom.NodeType');
+goog.require('goog.dom.Range');
+goog.require('goog.dom.TagName');
+goog.require('goog.dom.classlist');
+goog.require('goog.editor.Plugin');
+goog.require('goog.editor.style');
+goog.require('goog.events');
+goog.require('goog.events.EventHandler');
+goog.require('goog.events.EventType');
+goog.require('goog.events.KeyCodes');
+goog.require('goog.events.actionEventWrapper');
+goog.require('goog.functions');
+goog.require('goog.string.Unicode');
+goog.require('goog.ui.Component');
+goog.require('goog.ui.editor.Bubble');
+goog.require('goog.userAgent');
+
+
+
+/**
+ * Base class for bubble plugins. This is used for to connect user behavior
+ * in the editor to a goog.ui.editor.Bubble UI element that allows
+ * the user to modify the properties of an element on their page (e.g. the alt
+ * text of an image tag).
+ *
+ * Subclasses should override the abstract method 
getBubbleTargetFromSelection()
+ * with code to determine if the current selection should activate the bubble
+ * type. The other abstract method createBubbleContents() should be overriden
+ * with code to create the inside markup of the bubble.  The base class creates
+ * the rest of the bubble.
+ *
+ * @constructor
+ * @extends {goog.editor.Plugin}
+ */
+goog.editor.plugins.AbstractBubblePlugin = function() {
+  goog.editor.plugins.AbstractBubblePlugin.base(this, 'constructor');
+
+  /**
+   * Place to register events the plugin listens to.
+   * @type {goog.events.EventHandler<
+   *     !goog.editor.plugins.AbstractBubblePlugin>}
+   * @protected
+   */
+  this.eventRegister = new goog.events.EventHandler(this);
+
+  /**
+   * Instance factory function that creates a bubble UI component.  If set to a
+   * non-null value, this function will be used to create a bubble instead of
+   * the global factory function.  It takes as parameters the bubble parent
+   * element and the z index to draw the bubble at.
+   * @type {?function(!Element, number): !goog.ui.editor.Bubble}
+   * @private
+   */
+  this.bubbleFactory_ = null;
+};
+goog.inherits(goog.editor.plugins.AbstractBubblePlugin, goog.editor.Plugin);
+
+
+/**
+ * The css class name of option link elements.
+ * @type {string}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_ =
+    goog.getCssName('tr_option-link');
+
+
+/**
+ * The css class name of link elements.
+ * @type {string}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_ =
+    goog.getCssName('tr_bubble_link');
+
+
+/**
+ * A class name to mark elements that should be reachable by keyboard tabbing.
+ * @type {string}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_ =
+    goog.getCssName('tr_bubble_tabbable');
+
+
+/**
+ * The constant string used to separate option links.
+ * @type {string}
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING =
+    goog.string.Unicode.NBSP + '-' + goog.string.Unicode.NBSP;
+
+
+/**
+ * Default factory function for creating a bubble UI component.
+ * @param {!Element} parent The parent element for the bubble.
+ * @param {number} zIndex The z index to draw the bubble at.
+ * @return {!goog.ui.editor.Bubble} The new bubble component.
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_ = function(
+    parent, zIndex) {
+  return new goog.ui.editor.Bubble(parent, zIndex);
+};
+
+
+/**
+ * Global factory function that creates a bubble UI component. It takes as
+ * parameters the bubble parent element and the z index to draw the bubble at.
+ * @type {function(!Element, number): !goog.ui.editor.Bubble}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ =
+    goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_;
+
+
+/**
+ * Sets the global bubble factory function.
+ * @param {function(!Element, number): !goog.ui.editor.Bubble}
+ *     bubbleFactory Function that creates a bubble for the given bubble parent
+ *     element and z index.
+ */
+goog.editor.plugins.AbstractBubblePlugin.setBubbleFactory = function(
+    bubbleFactory) {
+  goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ = 
bubbleFactory;
+};
+
+
+/**
+ * Map from field id to shared bubble object.
+ * @type {!Object<goog.ui.editor.Bubble>}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.bubbleMap_ = {};
+
+
+/**
+ * The optional parent of the bubble.  If null or not set, we will use the
+ * application document. This is useful when you have an editor embedded in
+ * a scrolling DIV.
+ * @type {Element|undefined}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.bubbleParent_;
+
+
+/**
+ * The id of the panel this plugin added to the shared bubble.  Null when
+ * this plugin doesn't currently have a panel in a bubble.
+ * @type {string?}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.panelId_ = null;
+
+
+/**
+ * Whether this bubble should support tabbing through elements. False
+ * by default.
+ * @type {boolean}
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.keyboardNavigationEnabled_ =
+    false;
+
+
+/**
+ * Sets the instance bubble factory function.  If set to a non-null value, this
+ * function will be used to create a bubble instead of the global factory
+ * function.
+ * @param {?function(!Element, number): !goog.ui.editor.Bubble} bubbleFactory
+ *     Function that creates a bubble for the given bubble parent element and z
+ *     index.  Null to reset the factory function.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleFactory = function(
+    bubbleFactory) {
+  this.bubbleFactory_ = bubbleFactory;
+};
+
+
+/**
+ * Sets whether the bubble should support tabbing through elements.
+ * @param {boolean} keyboardNavigationEnabled
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.enableKeyboardNavigation =
+    function(keyboardNavigationEnabled) {
+  this.keyboardNavigationEnabled_ = keyboardNavigationEnabled;
+};
+
+
+/**
+ * Sets the bubble parent.
+ * @param {Element} bubbleParent An element where the bubble will be
+ *     anchored. If null, we will use the application document. This
+ *     is useful when you have an editor embedded in a scrolling div.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleParent = function(
+    bubbleParent) {
+  this.bubbleParent_ = bubbleParent;
+};
+
+
+/**
+ * Returns the bubble map.  Subclasses may override to use a separate map.
+ * @return {!Object<goog.ui.editor.Bubble>}
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleMap = function() {
+  return goog.editor.plugins.AbstractBubblePlugin.bubbleMap_;
+};
+
+
+/**
+ * @return {goog.dom.DomHelper} The dom helper for the bubble window.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleDom = function() {
+  return this.dom_;
+};
+
+
+/** @override */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getTrogClassId =
+    goog.functions.constant('AbstractBubblePlugin');
+
+
+/**
+ * Returns the element whose properties the bubble manipulates.
+ * @return {Element} The target element.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getTargetElement =
+    function() {
+  return this.targetElement_;
+};
+
+
+/** @override */
+goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyUp = function(e) {
+  // For example, when an image is selected, pressing any key overwrites
+  // the image and the panel should be hidden.
+  // Therefore we need to track key presses when the bubble is showing.
+  if (this.isVisible()) {
+    this.handleSelectionChange();
+  }
+  return false;
+};
+
+
+/**
+ * Pops up a property bubble for the given selection if appropriate and closes
+ * open property bubbles if no longer needed.  This should not be overridden.
+ * @override
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.handleSelectionChange =
+    function(opt_e, opt_target) {
+  var selectedElement;
+  if (opt_e) {
+    selectedElement = /** @type {Element} */ (opt_e.target);
+  } else if (opt_target) {
+    selectedElement = /** @type {Element} */ (opt_target);
+  } else {
+    var range = this.getFieldObject().getRange();
+    if (range) {
+      var startNode = range.getStartNode();
+      var endNode = range.getEndNode();
+      var startOffset = range.getStartOffset();
+      var endOffset = range.getEndOffset();
+      // Sometimes in IE, the range will be collapsed, but think the end node
+      // and start node are different (although in the same visible position).
+      // In this case, favor the position IE thinks is the start node.
+      if (goog.userAgent.IE && range.isCollapsed() && startNode != endNode) {
+        range = goog.dom.Range.createCaret(startNode, startOffset);
+      }
+      if (startNode.nodeType == goog.dom.NodeType.ELEMENT &&
+          startNode == endNode && startOffset == endOffset - 1) {
+        var element = startNode.childNodes[startOffset];
+        if (element.nodeType == goog.dom.NodeType.ELEMENT) {
+          selectedElement = element;
+        }
+      }
+    }
+    selectedElement = selectedElement || range && range.getContainerElement();
+  }
+  return this.handleSelectionChangeInternal(selectedElement);
+};
+
+
+/**
+ * Pops up a property bubble for the given selection if appropriate and closes
+ * open property bubbles if no longer needed.
+ * @param {Element?} selectedElement The selected element.
+ * @return {boolean} Always false, allowing every bubble plugin to handle the
+ *     event.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.
+    handleSelectionChangeInternal = function(selectedElement) {
+  if (selectedElement) {
+    var bubbleTarget = this.getBubbleTargetFromSelection(selectedElement);
+    if (bubbleTarget) {
+      if (bubbleTarget != this.targetElement_ || !this.panelId_) {
+        // Make sure any existing panel of the same type is closed before
+        // creating a new one.
+        if (this.panelId_) {
+          this.closeBubble();
+        }
+        this.createBubble(bubbleTarget);
+      }
+      return false;
+    }
+  }
+
+  if (this.panelId_) {
+    this.closeBubble();
+  }
+
+  return false;
+};
+
+
+/**
+ * Should be overriden by subclasses to return the bubble target element or
+ * null if an element of their required type isn't found.
+ * @param {Element} selectedElement The target of the selection change event or
+ *     the parent container of the current entire selection.
+ * @return {Element?} The HTML bubble target element or null if no element of
+ *     the required type is not found.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.
+    getBubbleTargetFromSelection = goog.abstractMethod;
+
+
+/** @override */
+goog.editor.plugins.AbstractBubblePlugin.prototype.disable = function(field) {
+  // When the field is made uneditable, dispose of the bubble.  We do this
+  // because the next time the field is made editable again it may be in
+  // a different document / iframe.
+  if (field.isUneditable()) {
+    var bubbleMap = this.getBubbleMap();
+    var bubble = bubbleMap[field.id];
+    if (bubble) {
+      if (field == this.getFieldObject()) {
+        this.closeBubble();
+      }
+      bubble.dispose();
+      delete bubbleMap[field.id];
+    }
+  }
+};
+
+
+/**
+ * @return {!goog.ui.editor.Bubble} The shared bubble object for the field this
+ *     plugin is registered on.  Creates it if necessary.
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getSharedBubble_ =
+    function() {
+  var bubbleParent = /** @type {!Element} */ (this.bubbleParent_ ||
+      this.getFieldObject().getAppWindow().document.body);
+  this.dom_ = goog.dom.getDomHelper(bubbleParent);
+
+  var bubbleMap = this.getBubbleMap();
+  var bubble = bubbleMap[this.getFieldObject().id];
+  if (!bubble) {
+    var factory = this.bubbleFactory_ ||
+        goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_;
+    bubble = factory.call(null, bubbleParent,
+        this.getFieldObject().getBaseZindex());
+    bubbleMap[this.getFieldObject().id] = bubble;
+  }
+  return bubble;
+};
+
+
+/**
+ * Creates and shows the property bubble.
+ * @param {Element} targetElement The target element of the bubble.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.createBubble = function(
+    targetElement) {
+  var bubble = this.getSharedBubble_();
+  if (!bubble.hasPanelOfType(this.getBubbleType())) {
+    this.targetElement_ = targetElement;
+
+    this.panelId_ = bubble.addPanel(this.getBubbleType(), 
this.getBubbleTitle(),
+        targetElement,
+        goog.bind(this.createBubbleContents, this),
+        this.shouldPreferBubbleAboveElement());
+    this.eventRegister.listen(bubble, goog.ui.Component.EventType.HIDE,
+        this.handlePanelClosed_);
+
+    this.onShow();
+
+    if (this.keyboardNavigationEnabled_) {
+      this.eventRegister.listen(bubble.getContentElement(),
+          goog.events.EventType.KEYDOWN, this.onBubbleKey_);
+    }
+  }
+};
+
+
+/**
+ * @return {string} The type of bubble shown by this plugin.  Usually the tag
+ *     name of the element this bubble targets.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleType = function() {
+  return '';
+};
+
+
+/**
+ * @return {string} The title for bubble shown by this plugin.  Defaults to no
+ *     title.  Should be overridden by subclasses.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleTitle = function() 
{
+  return '';
+};
+
+
+/**
+ * @return {boolean} Whether the bubble should prefer placement above the
+ *     target element.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.
+    shouldPreferBubbleAboveElement = goog.functions.FALSE;
+
+
+/**
+ * Should be overriden by subclasses to add the type specific contents to the
+ *     bubble.
+ * @param {Element} bubbleContainer The container element of the bubble to
+ *     which the contents should be added.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.createBubbleContents =
+    goog.abstractMethod;
+
+
+/**
+ * Register the handler for the target's CLICK event.
+ * @param {Element} target The event source element.
+ * @param {Function} handler The event handler.
+ * @protected
+ * @deprecated Use goog.editor.plugins.AbstractBubblePlugin.
+ *     registerActionHandler to register click and enter events.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.registerClickHandler =
+    function(target, handler) {
+  this.registerActionHandler(target, handler);
+};
+
+
+/**
+ * Register the handler for the target's CLICK and ENTER key events.
+ * @param {Element} target The event source element.
+ * @param {Function} handler The event handler.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.registerActionHandler =
+    function(target, handler) {
+  this.eventRegister.listenWithWrapper(target, goog.events.actionEventWrapper,
+      handler);
+};
+
+
+/**
+ * Closes the bubble.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.closeBubble = function() {
+  if (this.panelId_) {
+    this.getSharedBubble_().removePanel(this.panelId_);
+    this.handlePanelClosed_();
+  }
+};
+
+
+/**
+ * Called after the bubble is shown. The default implementation does nothing.
+ * Override it to provide your own one.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.onShow = goog.nullFunction;
+
+
+/**
+ * Called when the bubble is closed or hidden. The default implementation does
+ * nothing.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.cleanOnBubbleClose =
+    goog.nullFunction;
+
+
+/**
+ * Handles when the bubble panel is closed.  Invoked when the entire bubble is
+ * hidden and also directly when the panel is closed manually.
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.handlePanelClosed_ =
+    function() {
+  this.targetElement_ = null;
+  this.panelId_ = null;
+  this.eventRegister.removeAll();
+  this.cleanOnBubbleClose();
+};
+
+
+/**
+ * In case the keyboard navigation is enabled, this will set focus on the first
+ * tabbable element in the bubble when TAB is clicked.
+ * @override
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyDown = function(e) 
{
+  if (this.keyboardNavigationEnabled_ &&
+      this.isVisible() &&
+      e.keyCode == goog.events.KeyCodes.TAB && !e.shiftKey) {
+    var bubbleEl = this.getSharedBubble_().getContentElement();
+    var tabbable = goog.dom.getElementByClass(
+        goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, 
bubbleEl);
+    if (tabbable) {
+      tabbable.focus();
+      e.preventDefault();
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Handles a key event on the bubble. This ensures that the focus loops through
+ * the tabbable elements found in the bubble and then the focus is got by the
+ * field element.
+ * @param {goog.events.BrowserEvent} e The event.
+ * @private
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.onBubbleKey_ = function(e) {
+  if (this.isVisible() &&
+      e.keyCode == goog.events.KeyCodes.TAB) {
+    var bubbleEl = this.getSharedBubble_().getContentElement();
+    var tabbables = goog.dom.getElementsByClass(
+        goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, 
bubbleEl);
+    var tabbable = e.shiftKey ? tabbables[0] : goog.array.peek(tabbables);
+    var tabbingOutOfBubble = tabbable == e.target;
+    if (tabbingOutOfBubble) {
+      this.getFieldObject().focus();
+      e.preventDefault();
+    }
+  }
+};
+
+
+/**
+ * @return {boolean} Whether the bubble is visible.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.isVisible = function() {
+  return !!this.panelId_;
+};
+
+
+/**
+ * Reposition the property bubble.
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.reposition = function() {
+  var bubble = this.getSharedBubble_();
+  if (bubble) {
+    bubble.reposition();
+  }
+};
+
+
+/**
+ * Helper method that creates option links (such as edit, test, remove)
+ * @param {string} id String id for the span id.
+ * @return {Element} The option link element.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkOption = function(
+    id) {
+  // Dash plus link are together in a span so we can hide/show them easily
+  return this.dom_.createDom(goog.dom.TagName.SPAN,
+      {
+        id: id,
+        className:
+            goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_
+      },
+      this.dom_.createTextNode(
+          goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING));
+};
+
+
+/**
+ * Helper method that creates a link with text set to linkText and optionally
+ * wires up a listener for the CLICK event or the link. The link is navigable 
by
+ * tabs if {@code enableKeyboardNavigation(true)} was called.
+ * @param {string} linkId The id of the link.
+ * @param {string} linkText Text of the link.
+ * @param {Function=} opt_onClick Optional function to call when the link is
+ *     clicked.
+ * @param {Element=} opt_container If specified, location to insert link. If no
+ *     container is specified, the old link is removed and replaced.
+ * @return {Element} The link element.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.createLink = function(
+    linkId, linkText, opt_onClick, opt_container) {
+  var link = this.createLinkHelper(linkId, linkText, false, opt_container);
+  if (opt_onClick) {
+    this.registerActionHandler(link, opt_onClick);
+  }
+  return link;
+};
+
+
+/**
+ * Helper method to create a link to insert into the bubble. The link is
+ * navigable by tabs if {@code enableKeyboardNavigation(true)} was called.
+ * @param {string} linkId The id of the link.
+ * @param {string} linkText Text of the link.
+ * @param {boolean} isAnchor Set to true to create an actual anchor tag
+ *     instead of a span.  Actual links are right clickable (e.g. to open in
+ *     a new window) and also update window status on hover.
+ * @param {Element=} opt_container If specified, location to insert link. If no
+ *     container is specified, the old link is removed and replaced.
+ * @return {Element} The link element.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkHelper = function(
+    linkId, linkText, isAnchor, opt_container) {
+  var link = this.dom_.createDom(
+      isAnchor ? goog.dom.TagName.A : goog.dom.TagName.SPAN,
+      {className: goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_},
+      linkText);
+  if (this.keyboardNavigationEnabled_) {
+    this.setTabbable(link);
+  }
+  link.setAttribute('role', 'link');
+  this.setupLink(link, linkId, opt_container);
+  goog.editor.style.makeUnselectable(link, this.eventRegister);
+  return link;
+};
+
+
+/**
+ * Makes the given element tabbable.
+ *
+ * <p>Elements created by createLink[Helper] are tabbable even without
+ * calling this method. Call it for other elements if needed.
+ *
+ * <p>If tabindex is not already set in the element, this function sets it to 
0.
+ * You'll usually want to also call {@code enableKeyboardNavigation(true)}.
+ *
+ * @param {!Element} element
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.setTabbable =
+    function(element) {
+  if (!element.hasAttribute('tabindex')) {
+    element.setAttribute('tabindex', 0);
+  }
+  goog.dom.classlist.add(element,
+      goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_);
+};
+
+
+/**
+ * Inserts a link in the given container if it is specified or removes
+ * the old link with this id and replaces it with the new link
+ * @param {Element} link Html element to insert.
+ * @param {string} linkId Id of the link.
+ * @param {Element=} opt_container If specified, location to insert link.
+ * @protected
+ */
+goog.editor.plugins.AbstractBubblePlugin.prototype.setupLink = function(
+    link, linkId, opt_container) {
+  if (opt_container) {
+    opt_container.appendChild(link);
+  } else {
+    var oldLink = this.dom_.getElement(linkId);
+    if (oldLink) {
+      goog.dom.replaceNode(link, oldLink);
+    }
+  }
+
+  link.id = linkId;
+};

Reply via email to