Path: blob/trunk/third_party/closure/goog/editor/plugins/abstractbubbleplugin.js
1865 views
// Copyright 2005 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview Base class for bubble plugins.16* @author [email protected] (Robby Walker)17*/1819goog.provide('goog.editor.plugins.AbstractBubblePlugin');2021goog.require('goog.array');22goog.require('goog.dom');23goog.require('goog.dom.NodeType');24goog.require('goog.dom.Range');25goog.require('goog.dom.TagName');26goog.require('goog.dom.classlist');27goog.require('goog.editor.Plugin');28goog.require('goog.editor.style');29goog.require('goog.events');30goog.require('goog.events.EventHandler');31goog.require('goog.events.EventType');32goog.require('goog.events.KeyCodes');33goog.require('goog.events.actionEventWrapper');34goog.require('goog.functions');35goog.require('goog.string.Unicode');36goog.require('goog.ui.Component');37goog.require('goog.ui.editor.Bubble');38goog.require('goog.userAgent');39404142/**43* Base class for bubble plugins. This is used for to connect user behavior44* in the editor to a goog.ui.editor.Bubble UI element that allows45* the user to modify the properties of an element on their page (e.g. the alt46* text of an image tag).47*48* Subclasses should override the abstract method getBubbleTargetFromSelection()49* with code to determine if the current selection should activate the bubble50* type. The other abstract method createBubbleContents() should be overriden51* with code to create the inside markup of the bubble. The base class creates52* the rest of the bubble.53*54* @constructor55* @extends {goog.editor.Plugin}56*/57goog.editor.plugins.AbstractBubblePlugin = function() {58goog.editor.plugins.AbstractBubblePlugin.base(this, 'constructor');5960/**61* Place to register events the plugin listens to.62* @type {goog.events.EventHandler<63* !goog.editor.plugins.AbstractBubblePlugin>}64* @protected65*/66this.eventRegister = new goog.events.EventHandler(this);6768/**69* Instance factory function that creates a bubble UI component. If set to a70* non-null value, this function will be used to create a bubble instead of71* the global factory function. It takes as parameters the bubble parent72* element and the z index to draw the bubble at.73* @type {?function(!Element, number): !goog.ui.editor.Bubble}74* @private75*/76this.bubbleFactory_ = null;77};78goog.inherits(goog.editor.plugins.AbstractBubblePlugin, goog.editor.Plugin);798081/**82* The css class name of option link elements.83* @type {string}84* @private85*/86goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_ =87goog.getCssName('tr_option-link');888990/**91* The css class name of link elements.92* @type {string}93* @private94*/95goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_ =96goog.getCssName('tr_bubble_link');979899/**100* A class name to mark elements that should be reachable by keyboard tabbing.101* @type {string}102* @private103*/104goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_ =105goog.getCssName('tr_bubble_tabbable');106107108/**109* The constant string used to separate option links.110* @type {string}111* @protected112*/113goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING =114goog.string.Unicode.NBSP + '-' + goog.string.Unicode.NBSP;115116117/**118* Default factory function for creating a bubble UI component.119* @param {!Element} parent The parent element for the bubble.120* @param {number} zIndex The z index to draw the bubble at.121* @return {!goog.ui.editor.Bubble} The new bubble component.122* @private123*/124goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_ = function(125parent, zIndex) {126return new goog.ui.editor.Bubble(parent, zIndex);127};128129130/**131* Global factory function that creates a bubble UI component. It takes as132* parameters the bubble parent element and the z index to draw the bubble at.133* @type {function(!Element, number): !goog.ui.editor.Bubble}134* @private135*/136goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ =137goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_;138139140/**141* Sets the global bubble factory function.142* @param {function(!Element, number): !goog.ui.editor.Bubble}143* bubbleFactory Function that creates a bubble for the given bubble parent144* element and z index.145*/146goog.editor.plugins.AbstractBubblePlugin.setBubbleFactory = function(147bubbleFactory) {148goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ = bubbleFactory;149};150151152/**153* Map from field id to shared bubble object.154* @type {!Object<goog.ui.editor.Bubble>}155* @private156*/157goog.editor.plugins.AbstractBubblePlugin.bubbleMap_ = {};158159160/**161* The optional parent of the bubble. If null or not set, we will use the162* application document. This is useful when you have an editor embedded in163* a scrolling DIV.164* @type {Element|undefined}165* @private166*/167goog.editor.plugins.AbstractBubblePlugin.prototype.bubbleParent_;168169170/**171* The id of the panel this plugin added to the shared bubble. Null when172* this plugin doesn't currently have a panel in a bubble.173* @type {string?}174* @private175*/176goog.editor.plugins.AbstractBubblePlugin.prototype.panelId_ = null;177178179/**180* Whether this bubble should support tabbing through elements. False181* by default.182* @type {boolean}183* @private184*/185goog.editor.plugins.AbstractBubblePlugin.prototype.keyboardNavigationEnabled_ =186false;187188189/**190* Sets the instance bubble factory function. If set to a non-null value, this191* function will be used to create a bubble instead of the global factory192* function.193* @param {?function(!Element, number): !goog.ui.editor.Bubble} bubbleFactory194* Function that creates a bubble for the given bubble parent element and z195* index. Null to reset the factory function.196*/197goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleFactory = function(198bubbleFactory) {199this.bubbleFactory_ = bubbleFactory;200};201202203/**204* Sets whether the bubble should support tabbing through elements.205* @param {boolean} keyboardNavigationEnabled206*/207goog.editor.plugins.AbstractBubblePlugin.prototype.enableKeyboardNavigation =208function(keyboardNavigationEnabled) {209this.keyboardNavigationEnabled_ = keyboardNavigationEnabled;210};211212213/**214* Sets the bubble parent.215* @param {Element} bubbleParent An element where the bubble will be216* anchored. If null, we will use the application document. This217* is useful when you have an editor embedded in a scrolling div.218*/219goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleParent = function(220bubbleParent) {221this.bubbleParent_ = bubbleParent;222};223224225/**226* Returns the bubble map. Subclasses may override to use a separate map.227* @return {!Object<goog.ui.editor.Bubble>}228* @protected229*/230goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleMap = function() {231return goog.editor.plugins.AbstractBubblePlugin.bubbleMap_;232};233234235/**236* @return {goog.dom.DomHelper} The dom helper for the bubble window.237*/238goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleDom = function() {239return this.dom_;240};241242243/** @override */244goog.editor.plugins.AbstractBubblePlugin.prototype.getTrogClassId =245goog.functions.constant('AbstractBubblePlugin');246247248/**249* Returns the element whose properties the bubble manipulates.250* @return {Element} The target element.251*/252goog.editor.plugins.AbstractBubblePlugin.prototype.getTargetElement =253function() {254return this.targetElement_;255};256257258/** @override */259goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyUp = function(e) {260// For example, when an image is selected, pressing any key overwrites261// the image and the panel should be hidden.262// Therefore we need to track key presses when the bubble is showing.263if (this.isVisible()) {264this.handleSelectionChange();265}266return false;267};268269270/**271* Pops up a property bubble for the given selection if appropriate and closes272* open property bubbles if no longer needed. This should not be overridden.273* @override274*/275goog.editor.plugins.AbstractBubblePlugin.prototype.handleSelectionChange =276function(opt_e, opt_target) {277var selectedElement;278if (opt_e) {279selectedElement = /** @type {Element} */ (opt_e.target);280} else if (opt_target) {281selectedElement = /** @type {Element} */ (opt_target);282} else {283var range = this.getFieldObject().getRange();284if (range) {285var startNode = range.getStartNode();286var endNode = range.getEndNode();287var startOffset = range.getStartOffset();288var endOffset = range.getEndOffset();289// Sometimes in IE, the range will be collapsed, but think the end node290// and start node are different (although in the same visible position).291// In this case, favor the position IE thinks is the start node.292if (goog.userAgent.IE && range.isCollapsed() && startNode != endNode) {293range = goog.dom.Range.createCaret(startNode, startOffset);294}295if (startNode.nodeType == goog.dom.NodeType.ELEMENT &&296startNode == endNode && startOffset == endOffset - 1) {297var element = startNode.childNodes[startOffset];298if (element.nodeType == goog.dom.NodeType.ELEMENT) {299selectedElement = /** @type {!Element} */ (element);300}301}302}303selectedElement = selectedElement || range && range.getContainerElement();304}305return this.handleSelectionChangeInternal(selectedElement);306};307308309/**310* Pops up a property bubble for the given selection if appropriate and closes311* open property bubbles if no longer needed.312* @param {Element?} selectedElement The selected element.313* @return {boolean} Always false, allowing every bubble plugin to handle the314* event.315* @protected316*/317goog.editor.plugins.AbstractBubblePlugin.prototype318.handleSelectionChangeInternal = function(selectedElement) {319if (selectedElement) {320var bubbleTarget = this.getBubbleTargetFromSelection(selectedElement);321if (bubbleTarget) {322if (bubbleTarget != this.targetElement_ || !this.panelId_) {323// Make sure any existing panel of the same type is closed before324// creating a new one.325if (this.panelId_) {326this.closeBubble();327}328this.createBubble(bubbleTarget);329}330return false;331}332}333334if (this.panelId_) {335this.closeBubble();336}337338return false;339};340341342/**343* Should be overriden by subclasses to return the bubble target element or344* null if an element of their required type isn't found.345* @param {Element} selectedElement The target of the selection change event or346* the parent container of the current entire selection.347* @return {Element?} The HTML bubble target element or null if no element of348* the required type is not found.349*/350goog.editor.plugins.AbstractBubblePlugin.prototype351.getBubbleTargetFromSelection = goog.abstractMethod;352353354/** @override */355goog.editor.plugins.AbstractBubblePlugin.prototype.disable = function(field) {356// When the field is made uneditable, dispose of the bubble. We do this357// because the next time the field is made editable again it may be in358// a different document / iframe.359if (field.isUneditable()) {360var bubbleMap = this.getBubbleMap();361var bubble = bubbleMap[field.id];362if (bubble) {363if (field == this.getFieldObject()) {364this.closeBubble();365}366bubble.dispose();367delete bubbleMap[field.id];368}369}370};371372373/**374* @return {!goog.ui.editor.Bubble} The shared bubble object for the field this375* plugin is registered on. Creates it if necessary.376* @private377*/378goog.editor.plugins.AbstractBubblePlugin.prototype.getSharedBubble_ =379function() {380var bubbleParent = /** @type {!Element} */ (381this.bubbleParent_ || this.getFieldObject().getAppWindow().document.body);382this.dom_ = goog.dom.getDomHelper(bubbleParent);383384var bubbleMap = this.getBubbleMap();385var bubble = bubbleMap[this.getFieldObject().id];386if (!bubble) {387var factory = this.bubbleFactory_ ||388goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_;389bubble =390factory.call(null, bubbleParent, this.getFieldObject().getBaseZindex());391bubbleMap[this.getFieldObject().id] = bubble;392}393return bubble;394};395396397/**398* Creates and shows the property bubble.399* @param {Element} targetElement The target element of the bubble.400*/401goog.editor.plugins.AbstractBubblePlugin.prototype.createBubble = function(402targetElement) {403var bubble = this.getSharedBubble_();404if (!bubble.hasPanelOfType(this.getBubbleType())) {405this.targetElement_ = targetElement;406407this.panelId_ = bubble.addPanel(408this.getBubbleType(), this.getBubbleTitle(), targetElement,409goog.bind(this.createBubbleContents, this),410this.shouldPreferBubbleAboveElement());411this.eventRegister.listen(412bubble, goog.ui.Component.EventType.HIDE, this.handlePanelClosed_);413414this.onShow();415416if (this.keyboardNavigationEnabled_) {417this.eventRegister.listen(418bubble.getContentElement(), goog.events.EventType.KEYDOWN,419this.onBubbleKey_);420}421}422};423424425/**426* @return {string} The type of bubble shown by this plugin. Usually the tag427* name of the element this bubble targets.428* @protected429*/430goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleType = function() {431return '';432};433434435/**436* @return {string} The title for bubble shown by this plugin. Defaults to no437* title. Should be overridden by subclasses.438* @protected439*/440goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleTitle = function() {441return '';442};443444445/**446* @return {boolean} Whether the bubble should prefer placement above the447* target element.448* @protected449*/450goog.editor.plugins.AbstractBubblePlugin.prototype451.shouldPreferBubbleAboveElement = goog.functions.FALSE;452453454/**455* Should be overriden by subclasses to add the type specific contents to the456* bubble.457* @param {Element} bubbleContainer The container element of the bubble to458* which the contents should be added.459* @protected460*/461goog.editor.plugins.AbstractBubblePlugin.prototype.createBubbleContents =462goog.abstractMethod;463464465/**466* Register the handler for the target's CLICK event.467* @param {Element} target The event source element.468* @param {Function} handler The event handler.469* @protected470* @deprecated Use goog.editor.plugins.AbstractBubblePlugin.471* registerActionHandler to register click and enter events.472*/473goog.editor.plugins.AbstractBubblePlugin.prototype.registerClickHandler =474function(target, handler) {475this.registerActionHandler(target, handler);476};477478479/**480* Register the handler for the target's CLICK and ENTER key events.481* @param {Element} target The event source element.482* @param {Function} handler The event handler.483* @protected484*/485goog.editor.plugins.AbstractBubblePlugin.prototype.registerActionHandler =486function(target, handler) {487this.eventRegister.listenWithWrapper(488target, goog.events.actionEventWrapper, handler);489};490491492/**493* Closes the bubble.494*/495goog.editor.plugins.AbstractBubblePlugin.prototype.closeBubble = function() {496if (this.panelId_) {497this.getSharedBubble_().removePanel(this.panelId_);498this.handlePanelClosed_();499}500};501502503/**504* Called after the bubble is shown. The default implementation does nothing.505* Override it to provide your own one.506* @protected507*/508goog.editor.plugins.AbstractBubblePlugin.prototype.onShow = goog.nullFunction;509510511/**512* Called when the bubble is closed or hidden. The default implementation does513* nothing.514* @protected515*/516goog.editor.plugins.AbstractBubblePlugin.prototype.cleanOnBubbleClose =517goog.nullFunction;518519520/**521* Handles when the bubble panel is closed. Invoked when the entire bubble is522* hidden and also directly when the panel is closed manually.523* @private524*/525goog.editor.plugins.AbstractBubblePlugin.prototype.handlePanelClosed_ =526function() {527this.targetElement_ = null;528this.panelId_ = null;529this.eventRegister.removeAll();530this.cleanOnBubbleClose();531};532533534/**535* In case the keyboard navigation is enabled, this will set focus on the first536* tabbable element in the bubble when TAB is clicked.537* @override538*/539goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyDown = function(e) {540if (this.keyboardNavigationEnabled_ && this.isVisible() &&541e.keyCode == goog.events.KeyCodes.TAB && !e.shiftKey) {542var bubbleEl = this.getSharedBubble_().getContentElement();543var tabbable = goog.dom.getElementByClass(544goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, bubbleEl);545if (tabbable) {546tabbable.focus();547e.preventDefault();548return true;549}550}551return false;552};553554555/**556* Handles a key event on the bubble. This ensures that the focus loops through557* the tabbable elements found in the bubble and then the focus is got by the558* field element.559* @param {goog.events.BrowserEvent} e The event.560* @private561*/562goog.editor.plugins.AbstractBubblePlugin.prototype.onBubbleKey_ = function(e) {563if (this.isVisible() && e.keyCode == goog.events.KeyCodes.TAB) {564var bubbleEl = this.getSharedBubble_().getContentElement();565var tabbables = goog.dom.getElementsByClass(566goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, bubbleEl);567var tabbable = e.shiftKey ? tabbables[0] : goog.array.peek(tabbables);568var tabbingOutOfBubble = tabbable == e.target;569if (tabbingOutOfBubble) {570this.getFieldObject().focus();571e.preventDefault();572}573}574};575576577/**578* @return {boolean} Whether the bubble is visible.579*/580goog.editor.plugins.AbstractBubblePlugin.prototype.isVisible = function() {581return !!this.panelId_;582};583584585/**586* Reposition the property bubble.587*/588goog.editor.plugins.AbstractBubblePlugin.prototype.reposition = function() {589var bubble = this.getSharedBubble_();590if (bubble) {591bubble.reposition();592}593};594595596/**597* Helper method that creates option links (such as edit, test, remove)598* @param {string} id String id for the span id.599* @return {Element} The option link element.600* @protected601*/602goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkOption = function(603id) {604// Dash plus link are together in a span so we can hide/show them easily605return this.dom_.createDom(606goog.dom.TagName.SPAN, {607id: id,608className:609goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_610},611this.dom_.createTextNode(612goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING));613};614615616/**617* Helper method that creates a link with text set to linkText and optionally618* wires up a listener for the CLICK event or the link. The link is navigable by619* tabs if {@code enableKeyboardNavigation(true)} was called.620* @param {string} linkId The id of the link.621* @param {string} linkText Text of the link.622* @param {Function=} opt_onClick Optional function to call when the link is623* clicked.624* @param {Element=} opt_container If specified, location to insert link. If no625* container is specified, the old link is removed and replaced.626* @return {Element} The link element.627* @protected628*/629goog.editor.plugins.AbstractBubblePlugin.prototype.createLink = function(630linkId, linkText, opt_onClick, opt_container) {631var link = this.createLinkHelper(linkId, linkText, false, opt_container);632if (opt_onClick) {633this.registerActionHandler(link, opt_onClick);634}635return link;636};637638639/**640* Helper method to create a link to insert into the bubble. The link is641* navigable by tabs if {@code enableKeyboardNavigation(true)} was called.642* @param {string} linkId The id of the link.643* @param {string} linkText Text of the link.644* @param {boolean} isAnchor Set to true to create an actual anchor tag645* instead of a span. Actual links are right clickable (e.g. to open in646* a new window) and also update window status on hover.647* @param {Element=} opt_container If specified, location to insert link. If no648* container is specified, the old link is removed and replaced.649* @return {Element} The link element.650* @protected651*/652goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkHelper = function(653linkId, linkText, isAnchor, opt_container) {654var link = this.dom_.createDom(655isAnchor ? goog.dom.TagName.A : goog.dom.TagName.SPAN,656{className: goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_},657linkText);658if (this.keyboardNavigationEnabled_) {659this.setTabbable(link);660}661link.setAttribute('role', 'link');662this.setupLink(link, linkId, opt_container);663goog.editor.style.makeUnselectable(link, this.eventRegister);664return link;665};666667668/**669* Makes the given element tabbable.670*671* <p>Elements created by createLink[Helper] are tabbable even without672* calling this method. Call it for other elements if needed.673*674* <p>If tabindex is not already set in the element, this function sets it to 0.675* You'll usually want to also call {@code enableKeyboardNavigation(true)}.676*677* @param {!Element} element678* @protected679*/680goog.editor.plugins.AbstractBubblePlugin.prototype.setTabbable = function(681element) {682if (!element.hasAttribute('tabindex')) {683element.setAttribute('tabindex', 0);684}685goog.dom.classlist.add(686element, goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_);687};688689690/**691* Inserts a link in the given container if it is specified or removes692* the old link with this id and replaces it with the new link693* @param {Element} link Html element to insert.694* @param {string} linkId Id of the link.695* @param {Element=} opt_container If specified, location to insert link.696* @protected697*/698goog.editor.plugins.AbstractBubblePlugin.prototype.setupLink = function(699link, linkId, opt_container) {700if (opt_container) {701opt_container.appendChild(link);702} else {703var oldLink = this.dom_.getElement(linkId);704if (oldLink) {705goog.dom.replaceNode(link, oldLink);706}707}708709link.id = linkId;710};711712713