123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- // 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 robbyw@google.com (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 = /** @type {!Element} */ (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;
- };
|