Update wmts-hidpi, add nicer-api-docs
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
// 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.
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.AbstractBubblePlugin');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.dom.TagName');
|
||||
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.EventType');
|
||||
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.base(this);
|
||||
|
||||
/**
|
||||
* Place to register events the plugin listens to.
|
||||
* @type {goog.events.EventHandler}
|
||||
* @protected
|
||||
*/
|
||||
this.eventRegister = new goog.events.EventHandler(this);
|
||||
};
|
||||
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');
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.bubbleFactory_ =
|
||||
goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_;
|
||||
|
||||
|
||||
/**
|
||||
* Sets the 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.bubbleFactory_ = 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 the link elements. False
|
||||
* by default.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractBubblePlugin.prototype.keyboardNavigationEnabled_ =
|
||||
false;
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether the bubble should support tabbing through the link elements.
|
||||
* @param {boolean} keyboardNavigationEnabled Whether the bubble should support
|
||||
* tabbing through the link elements.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @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 bubble = goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[field.id];
|
||||
if (bubble) {
|
||||
bubble.dispose();
|
||||
delete goog.editor.plugins.AbstractBubblePlugin.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 bubble = goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[
|
||||
this.getFieldObject().id];
|
||||
if (!bubble) {
|
||||
bubble = goog.editor.plugins.AbstractBubblePlugin.bubbleFactory_.call(null,
|
||||
bubbleParent,
|
||||
this.getFieldObject().getBaseZindex());
|
||||
goog.editor.plugins.AbstractBubblePlugin.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;
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* In case the keyboard navigation is enabled, this will focus to the first link
|
||||
* element in the bubble when TAB is clicked. The user could still go through
|
||||
* the rest of tabbable UI elements using shift + TAB.
|
||||
* @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 linkEl = goog.dom.getElementByClass(
|
||||
goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_, bubbleEl);
|
||||
if (linkEl) {
|
||||
linkEl.focus();
|
||||
e.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles a key event on the bubble. This ensures that the focus loops through
|
||||
* the link 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 links = goog.dom.getElementsByClass(
|
||||
goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_, bubbleEl);
|
||||
var tabbingOutOfBubble = e.shiftKey ?
|
||||
links[0] == e.target :
|
||||
links.length && links[links.length - 1] == 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 optionaly
|
||||
* wires up a listener for the CLICK event or the link.
|
||||
* @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.
|
||||
* @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_) {
|
||||
link.setAttribute('tabindex', 0);
|
||||
}
|
||||
link.setAttribute('role', 'link');
|
||||
this.setupLink(link, linkId, opt_container);
|
||||
goog.editor.style.makeUnselectable(link, this.eventRegister);
|
||||
return link;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
@@ -0,0 +1,334 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview An abstract superclass for TrogEdit dialog plugins. Each
|
||||
* Trogedit dialog has its own plugin.
|
||||
*
|
||||
* @author nicksantos@google.com (Nick Santos)
|
||||
* @author marcosalmeida@google.com (Marcos Almeida)
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.AbstractDialogPlugin');
|
||||
goog.provide('goog.editor.plugins.AbstractDialogPlugin.EventType');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.editor.Field.EventType');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.ui.editor.AbstractDialog.EventType');
|
||||
|
||||
|
||||
// *** Public interface ***************************************************** //
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An abstract superclass for a Trogedit plugin that creates exactly one
|
||||
* dialog. By default dialogs are not reused -- each time execCommand is called,
|
||||
* a new instance of the dialog object is created (and the old one disposed of).
|
||||
* To enable reusing of the dialog object, subclasses should call
|
||||
* setReuseDialog() after calling the superclass constructor.
|
||||
* @param {string} command The command that this plugin handles.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin = function(command) {
|
||||
goog.editor.Plugin.call(this);
|
||||
this.command_ = command;
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.AbstractDialogPlugin, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.isSupportedCommand =
|
||||
function(command) {
|
||||
return command == this.command_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles execCommand. Dialog plugins don't make any changes when they open a
|
||||
* dialog, just when the dialog closes (because only modal dialogs are
|
||||
* supported). Hence this method does not dispatch the change events that the
|
||||
* superclass method does.
|
||||
* @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.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.execCommand = function(
|
||||
command, var_args) {
|
||||
return this.execCommandInternal.apply(this, arguments);
|
||||
};
|
||||
|
||||
|
||||
// *** Events *************************************************************** //
|
||||
|
||||
|
||||
/**
|
||||
* Event type constants for events the dialog plugins fire.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.EventType = {
|
||||
// This event is fired when a dialog has been opened.
|
||||
OPENED: 'dialogOpened',
|
||||
// This event is fired when a dialog has been closed.
|
||||
CLOSED: 'dialogClosed'
|
||||
};
|
||||
|
||||
|
||||
// *** Protected interface ************************************************** //
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of this plugin's dialog. Must be overridden by
|
||||
* subclasses.
|
||||
* @param {!goog.dom.DomHelper} dialogDomHelper The dom helper to be used to
|
||||
* create the dialog.
|
||||
* @param {*=} opt_arg The dialog specific argument. Concrete subclasses should
|
||||
* declare a specific type.
|
||||
* @return {goog.ui.editor.AbstractDialog} The newly created dialog.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.createDialog =
|
||||
goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current dialog that was created and opened by this plugin.
|
||||
* @return {goog.ui.editor.AbstractDialog} The current dialog that was created
|
||||
* and opened by this plugin.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.getDialog = function() {
|
||||
return this.dialog_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether this plugin should reuse the same instance of the dialog each
|
||||
* time execCommand is called or create a new one. This is intended for use by
|
||||
* subclasses only, hence protected.
|
||||
* @param {boolean} reuse Whether to reuse the dialog.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.setReuseDialog =
|
||||
function(reuse) {
|
||||
this.reuseDialog_ = reuse;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles execCommand by opening the dialog. Dispatches
|
||||
* {@link goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED} after the
|
||||
* dialog is shown.
|
||||
* @param {string} command The command to execute.
|
||||
* @param {*=} opt_arg The dialog specific argument. Should be the same as
|
||||
* {@link createDialog}.
|
||||
* @return {*} Always returns true, indicating the dialog was shown.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.execCommandInternal =
|
||||
function(command, opt_arg) {
|
||||
// If this plugin should not reuse dialog instances, first dispose of the
|
||||
// previous dialog.
|
||||
if (!this.reuseDialog_) {
|
||||
this.disposeDialog_();
|
||||
}
|
||||
// If there is no dialog yet (or we aren't reusing the previous one), create
|
||||
// one.
|
||||
if (!this.dialog_) {
|
||||
this.dialog_ = this.createDialog(
|
||||
// TODO(user): Add Field.getAppDomHelper. (Note dom helper will
|
||||
// need to be updated if setAppWindow is called by clients.)
|
||||
goog.dom.getDomHelper(this.getFieldObject().getAppWindow()),
|
||||
opt_arg);
|
||||
}
|
||||
|
||||
// Since we're opening a dialog, we need to clear the selection because the
|
||||
// focus will be going to the dialog, and if we leave an selection in the
|
||||
// editor while another selection is active in the dialog as the user is
|
||||
// typing, some browsers will screw up the original selection. But first we
|
||||
// save it so we can restore it when the dialog closes.
|
||||
// getRange may return null if there is no selection in the field.
|
||||
var tempRange = this.getFieldObject().getRange();
|
||||
// saveUsingDom() did not work as well as saveUsingNormalizedCarets(),
|
||||
// not sure why.
|
||||
this.savedRange_ = tempRange && goog.editor.range.saveUsingNormalizedCarets(
|
||||
tempRange);
|
||||
goog.dom.Range.clearSelection(
|
||||
this.getFieldObject().getEditableDomHelper().getWindow());
|
||||
|
||||
// Listen for the dialog closing so we can clean up.
|
||||
goog.events.listenOnce(this.dialog_,
|
||||
goog.ui.editor.AbstractDialog.EventType.AFTER_HIDE,
|
||||
this.handleAfterHide,
|
||||
false,
|
||||
this);
|
||||
|
||||
this.getFieldObject().setModalMode(true);
|
||||
this.dialog_.show();
|
||||
this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.OPENED);
|
||||
|
||||
// Since the selection has left the document, dispatch a selection
|
||||
// change event.
|
||||
this.getFieldObject().dispatchSelectionChangeEvent();
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Cleans up after the dialog has closed, including restoring the selection to
|
||||
* what it was before the dialog was opened. If a subclass modifies the editable
|
||||
* field's content such that the original selection is no longer valid (usually
|
||||
* the case when the user clicks OK, and sometimes also on Cancel), it is that
|
||||
* subclass' responsibility to place the selection in the desired place during
|
||||
* the OK or Cancel (or other) handler. In that case, this method will leave the
|
||||
* selection in place.
|
||||
* @param {goog.events.Event} e The AFTER_HIDE event object.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.handleAfterHide = function(
|
||||
e) {
|
||||
this.getFieldObject().setModalMode(false);
|
||||
this.restoreOriginalSelection();
|
||||
|
||||
if (!this.reuseDialog_) {
|
||||
this.disposeDialog_();
|
||||
}
|
||||
|
||||
this.dispatchEvent(goog.editor.plugins.AbstractDialogPlugin.EventType.CLOSED);
|
||||
|
||||
// Since the selection has returned to the document, dispatch a selection
|
||||
// change event.
|
||||
this.getFieldObject().dispatchSelectionChangeEvent();
|
||||
|
||||
// When the dialog closes due to pressing enter or escape, that happens on the
|
||||
// keydown event. But the browser will still fire a keyup event after that,
|
||||
// which is caught by the editable field and causes it to try to fire a
|
||||
// selection change event. To avoid that, we "debounce" the selection change
|
||||
// event, meaning the editable field will not fire that event if the keyup
|
||||
// that caused it immediately after this dialog was hidden ("immediately"
|
||||
// means a small number of milliseconds defined by the editable field).
|
||||
this.getFieldObject().debounceEvent(
|
||||
goog.editor.Field.EventType.SELECTIONCHANGE);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Restores the selection in the editable field to what it was before the dialog
|
||||
* was opened. This is not guaranteed to work if the contents of the field
|
||||
* have changed.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.restoreOriginalSelection =
|
||||
function() {
|
||||
this.getFieldObject().restoreSavedRange(this.savedRange_);
|
||||
this.savedRange_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Cleans up the structure used to save the original selection before the dialog
|
||||
* was opened. Should be used by subclasses that don't restore the original
|
||||
* selection via restoreOriginalSelection.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.disposeOriginalSelection =
|
||||
function() {
|
||||
if (this.savedRange_) {
|
||||
this.savedRange_.dispose();
|
||||
this.savedRange_ = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.disposeInternal =
|
||||
function() {
|
||||
this.disposeDialog_();
|
||||
goog.base(this, 'disposeInternal');
|
||||
};
|
||||
|
||||
|
||||
// *** Private implementation *********************************************** //
|
||||
|
||||
|
||||
/**
|
||||
* The command that this plugin handles.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.command_;
|
||||
|
||||
|
||||
/**
|
||||
* The current dialog that was created and opened by this plugin.
|
||||
* @type {goog.ui.editor.AbstractDialog}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.dialog_;
|
||||
|
||||
|
||||
/**
|
||||
* Whether this plugin should reuse the same instance of the dialog each time
|
||||
* execCommand is called or create a new one.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.reuseDialog_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Mutex to prevent recursive calls to disposeDialog_.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.isDisposingDialog_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* SavedRange representing the selection before the dialog was opened.
|
||||
* @type {goog.dom.SavedRange}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.savedRange_;
|
||||
|
||||
|
||||
/**
|
||||
* Disposes of the dialog if needed. It is this abstract class' responsibility
|
||||
* to dispose of the dialog. The "if needed" refers to the fact this method
|
||||
* might be called twice (nested calls, not sequential) in the dispose flow, so
|
||||
* if the dialog was already disposed once it should not be disposed again.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.AbstractDialogPlugin.prototype.disposeDialog_ = function() {
|
||||
// Wrap disposing the dialog in a mutex. Otherwise disposing it would cause it
|
||||
// to get hidden (if it is still open) and fire AFTER_HIDE, which in
|
||||
// turn would cause the dialog to be disposed again (closure only flags an
|
||||
// object as disposed after the dispose call chain completes, so it doesn't
|
||||
// prevent recursive dispose calls).
|
||||
if (this.dialog_ && !this.isDisposingDialog_) {
|
||||
this.isDisposingDialog_ = true;
|
||||
this.dialog_.dispose();
|
||||
this.dialog_ = null;
|
||||
this.isDisposingDialog_ = false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Abstract Editor plugin class to handle tab keys. Has one
|
||||
* abstract method which should be overriden to handle a tab key press.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author ajp@google.com (Andy Perelson)
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.AbstractTabHandler');
|
||||
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to handle tab keys. Specific tab behavior defined by subclasses.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.AbstractTabHandler = function() {
|
||||
goog.editor.Plugin.call(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.AbstractTabHandler, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.AbstractTabHandler.prototype.getTrogClassId =
|
||||
goog.abstractMethod;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.AbstractTabHandler.prototype.handleKeyboardShortcut =
|
||||
function(e, key, isModifierPressed) {
|
||||
// If a dialog doesn't have selectable field, Moz grabs the event and
|
||||
// performs actions in editor window. This solves that problem and allows
|
||||
// the event to be passed on to proper handlers.
|
||||
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't handle Ctrl+Tab since the user is most likely trying to switch
|
||||
// browser tabs. See bug 1305086.
|
||||
// FF3 on Mac sends Ctrl-Tab to trogedit and we end up inserting a tab, but
|
||||
// then it also switches the tabs. See bug 1511681. Note that we don't use
|
||||
// isModifierPressed here since isModifierPressed is true only if metaKey
|
||||
// is true on Mac.
|
||||
if (e.keyCode == goog.events.KeyCodes.TAB && !e.metaKey && !e.ctrlKey) {
|
||||
return this.handleTabKey(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle a tab key press.
|
||||
* @param {goog.events.Event} e The key event.
|
||||
* @return {boolean} Whether this event was handled by this plugin.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.AbstractTabHandler.prototype.handleTabKey =
|
||||
goog.abstractMethod;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview goog.editor plugin to handle splitting block quotes.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.Blockquote');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.dom.classes');
|
||||
goog.require('goog.editor.BrowserFeature');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.log');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to handle splitting block quotes. This plugin does nothing on its
|
||||
* own and should be used in conjunction with EnterHandler or one of its
|
||||
* subclasses.
|
||||
* @param {boolean} requiresClassNameToSplit Whether to split only blockquotes
|
||||
* that have the given classname.
|
||||
* @param {string=} opt_className The classname to apply to generated
|
||||
* blockquotes. Defaults to 'tr_bq'.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.Blockquote = function(requiresClassNameToSplit,
|
||||
opt_className) {
|
||||
goog.editor.Plugin.call(this);
|
||||
|
||||
/**
|
||||
* Whether we only split blockquotes that have {@link classname}, or whether
|
||||
* all blockquote tags should be split on enter.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.requiresClassNameToSplit_ = requiresClassNameToSplit;
|
||||
|
||||
/**
|
||||
* Classname to put on blockquotes that are generated via the toolbar for
|
||||
* blockquote, so that we can internally distinguish these from blockquotes
|
||||
* that are used for indentation. This classname can be over-ridden by
|
||||
* clients for styling or other purposes.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.className_ = opt_className || goog.getCssName('tr_bq');
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.Blockquote, goog.editor.Plugin);
|
||||
|
||||
|
||||
/**
|
||||
* Command implemented by this plugin.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.SPLIT_COMMAND = '+splitBlockquote';
|
||||
|
||||
|
||||
/**
|
||||
* Class ID used to identify this plugin.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.CLASS_ID = 'Blockquote';
|
||||
|
||||
|
||||
/**
|
||||
* Logging object.
|
||||
* @type {goog.log.Logger}
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.logger =
|
||||
goog.log.getLogger('goog.editor.plugins.Blockquote');
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.Blockquote.prototype.getTrogClassId = function() {
|
||||
return goog.editor.plugins.Blockquote.CLASS_ID;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Since our exec command is always called from elsewhere, we make it silent.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.isSilentCommand = goog.functions.TRUE;
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a node is a blockquote node. If isAlreadySetup is set, it also
|
||||
* makes sure the node has the blockquote classname applied. Otherwise, it
|
||||
* ensures that the blockquote does not already have the classname applied.
|
||||
* @param {Node} node DOM node to check.
|
||||
* @param {boolean} isAlreadySetup True to enforce that the classname must be
|
||||
* set in order for it to count as a blockquote, false to
|
||||
* enforce that the classname must not be set in order for
|
||||
* it to count as a blockquote.
|
||||
* @param {boolean} requiresClassNameToSplit Whether only blockquotes with the
|
||||
* class name should be split.
|
||||
* @param {string} className The official blockquote class name.
|
||||
* @return {boolean} Whether node is a blockquote and if isAlreadySetup is
|
||||
* true, then whether this is a setup blockquote.
|
||||
* @deprecated Use {@link #isSplittableBlockquote},
|
||||
* {@link #isSetupBlockquote}, or {@link #isUnsetupBlockquote} instead
|
||||
* since this has confusing behavior.
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.isBlockquote = function(node, isAlreadySetup,
|
||||
requiresClassNameToSplit, className) {
|
||||
if (node.tagName != goog.dom.TagName.BLOCKQUOTE) {
|
||||
return false;
|
||||
}
|
||||
if (!requiresClassNameToSplit) {
|
||||
return isAlreadySetup;
|
||||
}
|
||||
var hasClassName = goog.dom.classes.has(/** @type {Element} */ (node),
|
||||
className);
|
||||
return isAlreadySetup ? hasClassName : !hasClassName;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a node is a blockquote which can be split. A splittable blockquote
|
||||
* meets the following criteria:
|
||||
* <ol>
|
||||
* <li>Node is a blockquote element</li>
|
||||
* <li>Node has the blockquote classname if the classname is required to
|
||||
* split</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param {Node} node DOM node in question.
|
||||
* @return {boolean} Whether the node is a splittable blockquote.
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.isSplittableBlockquote =
|
||||
function(node) {
|
||||
if (node.tagName != goog.dom.TagName.BLOCKQUOTE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.requiresClassNameToSplit_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return goog.dom.classes.has(node, this.className_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a node is a blockquote element which has been setup.
|
||||
* @param {Node} node DOM node to check.
|
||||
* @return {boolean} Whether the node is a blockquote with the required class
|
||||
* name applied.
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.isSetupBlockquote =
|
||||
function(node) {
|
||||
return node.tagName == goog.dom.TagName.BLOCKQUOTE &&
|
||||
goog.dom.classes.has(node, this.className_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a node is a blockquote element which has not been setup yet.
|
||||
* @param {Node} node DOM node to check.
|
||||
* @return {boolean} Whether the node is a blockquote without the required
|
||||
* class name applied.
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.isUnsetupBlockquote =
|
||||
function(node) {
|
||||
return node.tagName == goog.dom.TagName.BLOCKQUOTE &&
|
||||
!this.isSetupBlockquote(node);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the class name required for setup blockquotes.
|
||||
* @return {string} The blockquote class name.
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.getBlockquoteClassName = function() {
|
||||
return this.className_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Helper routine which walks up the tree to find the topmost
|
||||
* ancestor with only a single child. The ancestor node or the original
|
||||
* node (if no ancestor was found) is then removed from the DOM.
|
||||
*
|
||||
* @param {Node} node The node whose ancestors have to be searched.
|
||||
* @param {Node} root The root node to stop the search at.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_ = function(
|
||||
node, root) {
|
||||
var predicateFunc = function(parentNode) {
|
||||
return parentNode != root && parentNode.childNodes.length == 1;
|
||||
};
|
||||
var ancestor = goog.editor.node.findHighestMatchingAncestor(node,
|
||||
predicateFunc);
|
||||
if (!ancestor) {
|
||||
ancestor = node;
|
||||
}
|
||||
goog.dom.removeNode(ancestor);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove every nodes from the DOM tree that are all white space nodes.
|
||||
* @param {Array.<Node>} nodes Nodes to be checked.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_ = function(nodes) {
|
||||
for (var i = 0; i < nodes.length; ++i) {
|
||||
if (goog.editor.node.isEmpty(nodes[i], true)) {
|
||||
goog.dom.removeNode(nodes[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.Blockquote.prototype.isSupportedCommand = function(
|
||||
command) {
|
||||
return command == goog.editor.plugins.Blockquote.SPLIT_COMMAND;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Splits a quoted region if any. To be called on a key press event. When this
|
||||
* function returns true, the event that caused it to be called should be
|
||||
* canceled.
|
||||
* @param {string} command The command to execute.
|
||||
* @param {...*} var_args Single additional argument representing the
|
||||
* current cursor position. In IE, it is a single node. In any other
|
||||
* browser, it is an object with a {@code node} key and an {@code offset}
|
||||
* key.
|
||||
* @return {boolean|undefined} Boolean true when the quoted region has been
|
||||
* split, false or undefined otherwise.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.execCommandInternal = function(
|
||||
command, var_args) {
|
||||
var pos = arguments[1];
|
||||
if (command == goog.editor.plugins.Blockquote.SPLIT_COMMAND && pos &&
|
||||
(this.className_ || !this.requiresClassNameToSplit_)) {
|
||||
return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
|
||||
this.splitQuotedBlockW3C_(pos) :
|
||||
this.splitQuotedBlockIE_(/** @type {Node} */ (pos));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Version of splitQuotedBlock_ that uses W3C ranges.
|
||||
* @param {Object} anchorPos The current cursor position.
|
||||
* @return {boolean} Whether the blockquote was split.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.splitQuotedBlockW3C_ =
|
||||
function(anchorPos) {
|
||||
var cursorNode = anchorPos.node;
|
||||
var quoteNode = goog.editor.node.findTopMostEditableAncestor(
|
||||
cursorNode.parentNode, goog.bind(this.isSplittableBlockquote, this));
|
||||
|
||||
var secondHalf, textNodeToRemove;
|
||||
var insertTextNode = false;
|
||||
// There are two special conditions that we account for here.
|
||||
//
|
||||
// 1. Whenever the cursor is after (one<BR>|) or just before a BR element
|
||||
// (one|<BR>) and the user presses enter, the second quoted block starts
|
||||
// with a BR which appears to the user as an extra newline. This stems
|
||||
// from the fact that we create two text nodes as our split boundaries
|
||||
// and the BR becomes a part of the second half because of this.
|
||||
//
|
||||
// 2. When the cursor is at the end of a text node with no siblings and
|
||||
// the user presses enter, the second blockquote might contain a
|
||||
// empty subtree that ends in a 0 length text node. We account for that
|
||||
// as a post-splitting operation.
|
||||
if (quoteNode) {
|
||||
|
||||
// selection is in a line that has text in it
|
||||
if (cursorNode.nodeType == goog.dom.NodeType.TEXT) {
|
||||
if (anchorPos.offset == cursorNode.length) {
|
||||
var siblingNode = cursorNode.nextSibling;
|
||||
|
||||
// This accounts for the condition where the cursor appears at the
|
||||
// end of a text node and right before the BR eg: one|<BR>. We ensure
|
||||
// that we split on the BR in that case.
|
||||
if (siblingNode && siblingNode.tagName == goog.dom.TagName.BR) {
|
||||
cursorNode = siblingNode;
|
||||
// This might be null but splitDomTreeAt accounts for the null case.
|
||||
secondHalf = siblingNode.nextSibling;
|
||||
} else {
|
||||
textNodeToRemove = cursorNode.splitText(anchorPos.offset);
|
||||
secondHalf = textNodeToRemove;
|
||||
}
|
||||
} else {
|
||||
secondHalf = cursorNode.splitText(anchorPos.offset);
|
||||
}
|
||||
} else if (cursorNode.tagName == goog.dom.TagName.BR) {
|
||||
// This might be null but splitDomTreeAt accounts for the null case.
|
||||
secondHalf = cursorNode.nextSibling;
|
||||
} else {
|
||||
// The selection is in a line that is empty, with more than 1 level
|
||||
// of quote.
|
||||
insertTextNode = true;
|
||||
}
|
||||
} else {
|
||||
// Check if current node is a quote node.
|
||||
// This will happen if user clicks in an empty line in the quote,
|
||||
// when there is 1 level of quote.
|
||||
if (this.isSetupBlockquote(cursorNode)) {
|
||||
quoteNode = cursorNode;
|
||||
insertTextNode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertTextNode) {
|
||||
// Create two empty text nodes to split between.
|
||||
cursorNode = this.insertEmptyTextNodeBeforeRange_();
|
||||
secondHalf = this.insertEmptyTextNodeBeforeRange_();
|
||||
}
|
||||
|
||||
if (!quoteNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
secondHalf = goog.editor.node.splitDomTreeAt(cursorNode, secondHalf,
|
||||
quoteNode);
|
||||
goog.dom.insertSiblingAfter(secondHalf, quoteNode);
|
||||
|
||||
// Set the insertion point.
|
||||
var dh = this.getFieldDomHelper();
|
||||
var tagToInsert =
|
||||
this.getFieldObject().queryCommandValue(
|
||||
goog.editor.Command.DEFAULT_TAG) ||
|
||||
goog.dom.TagName.DIV;
|
||||
var container = dh.createElement(/** @type {string} */ (tagToInsert));
|
||||
container.innerHTML = ' '; // Prevent the div from collapsing.
|
||||
quoteNode.parentNode.insertBefore(container, secondHalf);
|
||||
dh.getWindow().getSelection().collapse(container, 0);
|
||||
|
||||
// We need to account for the condition where the second blockquote
|
||||
// might contain an empty DOM tree. This arises from trying to split
|
||||
// at the end of an empty text node. We resolve this by walking up the tree
|
||||
// till we either reach the blockquote or till we hit a node with more
|
||||
// than one child. The resulting node is then removed from the DOM.
|
||||
if (textNodeToRemove) {
|
||||
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
|
||||
textNodeToRemove, secondHalf);
|
||||
}
|
||||
|
||||
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
|
||||
[quoteNode, secondHalf]);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inserts an empty text node before the field's range.
|
||||
* @return {!Node} The empty text node.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.insertEmptyTextNodeBeforeRange_ =
|
||||
function() {
|
||||
var range = this.getFieldObject().getRange();
|
||||
var node = this.getFieldDomHelper().createTextNode('');
|
||||
range.insertNode(node, true);
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* IE version of splitQuotedBlock_.
|
||||
* @param {Node} splitNode The current cursor position.
|
||||
* @return {boolean} Whether the blockquote was split.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.Blockquote.prototype.splitQuotedBlockIE_ =
|
||||
function(splitNode) {
|
||||
var dh = this.getFieldDomHelper();
|
||||
var quoteNode = goog.editor.node.findTopMostEditableAncestor(
|
||||
splitNode.parentNode, goog.bind(this.isSplittableBlockquote, this));
|
||||
|
||||
if (!quoteNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var clone = splitNode.cloneNode(false);
|
||||
|
||||
// Whenever the cursor is just before a BR element (one|<BR>) and the user
|
||||
// presses enter, the second quoted block starts with a BR which appears
|
||||
// to the user as an extra newline. This stems from the fact that the
|
||||
// dummy span that we create (splitNode) occurs before the BR and we split
|
||||
// on that.
|
||||
if (splitNode.nextSibling &&
|
||||
splitNode.nextSibling.tagName == goog.dom.TagName.BR) {
|
||||
splitNode = splitNode.nextSibling;
|
||||
}
|
||||
var secondHalf = goog.editor.node.splitDomTreeAt(splitNode, clone, quoteNode);
|
||||
goog.dom.insertSiblingAfter(secondHalf, quoteNode);
|
||||
|
||||
// Set insertion point.
|
||||
var tagToInsert =
|
||||
this.getFieldObject().queryCommandValue(
|
||||
goog.editor.Command.DEFAULT_TAG) ||
|
||||
goog.dom.TagName.DIV;
|
||||
var div = dh.createElement(/** @type {string} */ (tagToInsert));
|
||||
quoteNode.parentNode.insertBefore(div, secondHalf);
|
||||
|
||||
// The div needs non-whitespace contents in order for the insertion point
|
||||
// to get correctly inserted.
|
||||
div.innerHTML = ' ';
|
||||
|
||||
// Moving the range 1 char isn't enough when you have markup.
|
||||
// This moves the range to the end of the nbsp.
|
||||
var range = dh.getDocument().selection.createRange();
|
||||
range.moveToElementText(splitNode);
|
||||
range.move('character', 2);
|
||||
range.select();
|
||||
|
||||
// Remove the no-longer-necessary nbsp.
|
||||
div.innerHTML = '';
|
||||
|
||||
// Clear the original selection.
|
||||
range.pasteHTML('');
|
||||
|
||||
// We need to remove clone from the DOM but just removing clone alone will
|
||||
// not suffice. Let's assume we have the following DOM structure and the
|
||||
// cursor is placed after the first numbered list item "one".
|
||||
//
|
||||
// <blockquote class="gmail-quote">
|
||||
// <div><div>a</div><ol><li>one|</li></ol></div>
|
||||
// <div>b</div>
|
||||
// </blockquote>
|
||||
//
|
||||
// After pressing enter, we have the following structure.
|
||||
//
|
||||
// <blockquote class="gmail-quote">
|
||||
// <div><div>a</div><ol><li>one|</li></ol></div>
|
||||
// </blockquote>
|
||||
// <div> </div>
|
||||
// <blockquote class="gmail-quote">
|
||||
// <div><ol><li><span id=""></span></li></ol></div>
|
||||
// <div>b</div>
|
||||
// </blockquote>
|
||||
//
|
||||
// The clone is contained in a subtree which should be removed. This stems
|
||||
// from the fact that we invoke splitDomTreeAt with the dummy span
|
||||
// as the starting splitting point and this results in the empty subtree
|
||||
// <div><ol><li><span id=""></span></li></ol></div>.
|
||||
//
|
||||
// We resolve this by walking up the tree till we either reach the
|
||||
// blockquote or till we hit a node with more than one child. The resulting
|
||||
// node is then removed from the DOM.
|
||||
goog.editor.plugins.Blockquote.findAndRemoveSingleChildAncestor_(
|
||||
clone, secondHalf);
|
||||
|
||||
goog.editor.plugins.Blockquote.removeAllWhiteSpaceNodes_(
|
||||
[quoteNode, secondHalf]);
|
||||
return true;
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
// 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.
|
||||
// All Rights Reserved
|
||||
|
||||
/**
|
||||
* @fileoverview Plugin for generating emoticons.
|
||||
*
|
||||
* @author nicksantos@google.com (Nick Santos)
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.Emoticons');
|
||||
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.ui.emoji.Emoji');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin for generating emoticons.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.Emoticons = function() {
|
||||
goog.base(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.Emoticons, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** The emoticon command. */
|
||||
goog.editor.plugins.Emoticons.COMMAND = '+emoticon';
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.Emoticons.prototype.getTrogClassId =
|
||||
goog.functions.constant(goog.editor.plugins.Emoticons.COMMAND);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.Emoticons.prototype.isSupportedCommand = function(
|
||||
command) {
|
||||
return command == goog.editor.plugins.Emoticons.COMMAND;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inserts an emoticon into the editor at the cursor location. Places the
|
||||
* cursor to the right of the inserted emoticon.
|
||||
* @param {string} command Command to execute.
|
||||
* @param {*=} opt_arg Emoji to insert.
|
||||
* @return {Object|undefined} The result of the command.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.Emoticons.prototype.execCommandInternal = function(
|
||||
command, opt_arg) {
|
||||
var emoji = /** @type {goog.ui.emoji.Emoji} */ (opt_arg);
|
||||
var dom = this.getFieldDomHelper();
|
||||
var img = dom.createDom(goog.dom.TagName.IMG, {
|
||||
'src': emoji.getUrl(),
|
||||
'style': 'margin:0 0.2ex;vertical-align:middle'
|
||||
});
|
||||
img.setAttribute(goog.ui.emoji.Emoji.ATTRIBUTE, emoji.getId());
|
||||
|
||||
this.getFieldObject().getRange().replaceContentsWithNode(img);
|
||||
|
||||
// IE8 does the right thing with the cursor, and has a js error when we try
|
||||
// to place the cursor manually.
|
||||
// IE9 loses the cursor when the window is focused, so focus first.
|
||||
if (!goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(9)) {
|
||||
this.getFieldObject().focus();
|
||||
goog.editor.range.placeCursorNextTo(img, false);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,767 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Plugin to handle enter keys.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.EnterHandler');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeOffset');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.BrowserFeature');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.editor.plugins.Blockquote');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.editor.style');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.object');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to handle enter keys. This does all the crazy to normalize (as much as
|
||||
* is reasonable) what happens when you hit enter. This also handles the
|
||||
* special casing of hitting enter in a blockquote.
|
||||
*
|
||||
* In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In
|
||||
* Firefox, the resulting HTML uses BR tags at the end of each line.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler = function() {
|
||||
goog.editor.Plugin.call(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);
|
||||
|
||||
|
||||
/**
|
||||
* The type of block level tag to add on enter, for browsers that support
|
||||
* specifying the default block-level tag. Can be overriden by subclasses; must
|
||||
* be either DIV or P.
|
||||
* @type {goog.dom.TagName}
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {
|
||||
return 'EnterHandler';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {
|
||||
goog.base(this, 'enable', fieldObject);
|
||||
|
||||
if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&
|
||||
(this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {
|
||||
var doc = this.getFieldDomHelper().getDocument();
|
||||
doc.execCommand('opera-defaultBlock', false, this.tag);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If the contents are empty, return the 'default' html for the field.
|
||||
* The 'default' contents depend on the enter handling mode, so it
|
||||
* makes the most sense in this plugin.
|
||||
* @param {string} html The html to prepare.
|
||||
* @return {string} The original HTML, or default contents if that
|
||||
* html is empty.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(
|
||||
html) {
|
||||
if (!html || goog.string.isBreakingWhitespace(html)) {
|
||||
return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?
|
||||
this.getNonCollapsingBlankHtml() : '';
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets HTML with no contents that won't collapse, for browsers that
|
||||
* collapse the empty string.
|
||||
* @return {string} Blank html.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =
|
||||
goog.functions.constant('<br>');
|
||||
|
||||
|
||||
/**
|
||||
* Internal backspace handler.
|
||||
* @param {goog.events.Event} e The keypress event.
|
||||
* @param {goog.dom.AbstractRange} range The closure range object.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(e,
|
||||
range) {
|
||||
var field = this.getFieldObject().getElement();
|
||||
var container = range && range.getStartNode();
|
||||
|
||||
if (field.firstChild == container && goog.editor.node.isEmpty(container)) {
|
||||
e.preventDefault();
|
||||
// TODO(user): I think we probably don't need to stopPropagation here
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Fix paragraphs to be the correct type of node.
|
||||
* @param {goog.events.Event} e The <enter> key event.
|
||||
* @param {boolean} split Whether we already split up a blockquote by
|
||||
* manually inserting elements.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =
|
||||
function(e, split) {
|
||||
// Force IE to turn the node we are leaving into a DIV. If we do turn
|
||||
// it into a DIV, the node IE creates in response to ENTER will also be
|
||||
// a DIV. If we don't, it will be a P. We handle that case
|
||||
// in handleKeyUpIE_
|
||||
if (goog.userAgent.IE || goog.userAgent.OPERA) {
|
||||
this.ensureBlockIeOpera(goog.dom.TagName.DIV);
|
||||
} else if (!split && goog.userAgent.WEBKIT) {
|
||||
// WebKit duplicates a blockquote when the user hits enter. Let's cancel
|
||||
// this and insert a BR instead, to make it more consistent with the other
|
||||
// browsers.
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (!range || !goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(
|
||||
range.getContainerElement())) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dh = this.getFieldDomHelper();
|
||||
var br = dh.createElement(goog.dom.TagName.BR);
|
||||
range.insertNode(br, true);
|
||||
|
||||
// If the BR is at the end of a block element, Safari still thinks there is
|
||||
// only one line instead of two, so we need to add another BR in that case.
|
||||
if (goog.editor.node.isBlockTag(br.parentNode) &&
|
||||
!goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {
|
||||
goog.dom.insertSiblingBefore(
|
||||
dh.createElement(goog.dom.TagName.BR), br);
|
||||
}
|
||||
|
||||
goog.editor.range.placeCursorNextTo(br, false);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether the lowest containing block node is a blockquote.
|
||||
* @param {Node} n The node.
|
||||
* @return {boolean} Whether the deepest block ancestor of n is a blockquote.
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {
|
||||
for (var current = n; current; current = current.parentNode) {
|
||||
if (goog.editor.node.isBlockTag(current)) {
|
||||
return current.tagName == goog.dom.TagName.BLOCKQUOTE;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Internal delete key handler.
|
||||
* @param {goog.events.Event} e The keypress event.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {
|
||||
this.deleteBrGecko(e);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the element at the cursor if it is a BR node, and if it does, calls
|
||||
* e.preventDefault to stop the browser from deleting. Only necessary in Gecko
|
||||
* as a workaround for mozilla bug 205350 where deleting a BR that is followed
|
||||
* by a block element doesn't work (the BR gets immediately replaced). We also
|
||||
* need to account for an ill-formed cursor which occurs from us trying to
|
||||
* stop the browser from deleting.
|
||||
*
|
||||
* @param {goog.events.Event} e The DELETE keypress event.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (range.isCollapsed()) {
|
||||
var container = range.getEndNode();
|
||||
if (container.nodeType == goog.dom.NodeType.ELEMENT) {
|
||||
var nextNode = container.childNodes[range.getEndOffset()];
|
||||
if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {
|
||||
// We want to retrieve the first non-whitespace previous sibling
|
||||
// as we could have added an empty text node below and want to
|
||||
// properly handle deleting a sequence of BR's.
|
||||
var previousSibling = goog.editor.node.getPreviousSibling(nextNode);
|
||||
var nextSibling = nextNode.nextSibling;
|
||||
|
||||
container.removeChild(nextNode);
|
||||
e.preventDefault();
|
||||
|
||||
// When we delete a BR followed by a block level element, the cursor
|
||||
// has a line-height which spans the height of the block level element.
|
||||
// e.g. If we delete a BR followed by a UL, the resulting HTML will
|
||||
// appear to the end user like:-
|
||||
//
|
||||
// | * one
|
||||
// | * two
|
||||
// | * three
|
||||
//
|
||||
// There are a couple of cases that we have to account for in order to
|
||||
// properly conform to what the user expects when DELETE is pressed.
|
||||
//
|
||||
// 1. If the BR has a previous sibling and the previous sibling is
|
||||
// not a block level element or a BR, we place the cursor at the
|
||||
// end of that.
|
||||
// 2. If the BR doesn't have a previous sibling or the previous sibling
|
||||
// is a block level element or a BR, we place the cursor at the
|
||||
// beginning of the leftmost leaf of its next sibling.
|
||||
if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {
|
||||
if (previousSibling &&
|
||||
!(previousSibling.tagName == goog.dom.TagName.BR ||
|
||||
goog.editor.node.isBlockTag(previousSibling))) {
|
||||
goog.dom.Range.createCaret(
|
||||
previousSibling,
|
||||
goog.editor.node.getLength(previousSibling)).select();
|
||||
} else {
|
||||
var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);
|
||||
goog.dom.Range.createCaret(leftMostLeaf, 0).select();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {
|
||||
// If a dialog doesn't have selectable field, Gecko grabs the event and
|
||||
// performs actions in editor window. This solves that problem and allows
|
||||
// the event to be passed on to proper handlers.
|
||||
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Firefox will allow the first node in an iframe to be deleted
|
||||
// on a backspace. Disallow it if the node is empty.
|
||||
if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
|
||||
this.handleBackspaceInternal(e, this.getFieldObject().getRange());
|
||||
|
||||
} else if (e.keyCode == goog.events.KeyCodes.ENTER) {
|
||||
if (goog.userAgent.GECKO) {
|
||||
if (!e.shiftKey) {
|
||||
// Behave similarly to IE's content editable return carriage:
|
||||
// If the shift key is down or specified by the application, insert a
|
||||
// BR, otherwise split paragraphs
|
||||
this.handleEnterGecko_(e);
|
||||
}
|
||||
} else {
|
||||
// In Gecko-based browsers, this is handled in the handleEnterGecko_
|
||||
// method.
|
||||
this.getFieldObject().dispatchBeforeChange();
|
||||
var cursorPosition = this.deleteCursorSelection_();
|
||||
|
||||
var split = !!this.getFieldObject().execCommand(
|
||||
goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
|
||||
if (split) {
|
||||
// TODO(user): I think we probably don't need to stopPropagation here
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
this.releasePositionObject_(cursorPosition);
|
||||
|
||||
if (goog.userAgent.WEBKIT) {
|
||||
this.handleEnterWebkitInternal(e);
|
||||
}
|
||||
|
||||
this.processParagraphTagsInternal(e, split);
|
||||
this.getFieldObject().dispatchChange();
|
||||
}
|
||||
|
||||
} else if (goog.userAgent.GECKO && e.keyCode == goog.events.KeyCodes.DELETE) {
|
||||
this.handleDeleteGecko(e);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {
|
||||
// If a dialog doesn't have selectable field, Gecko grabs the event and
|
||||
// performs actions in editor window. This solves that problem and allows
|
||||
// the event to be passed on to proper handlers.
|
||||
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
|
||||
return false;
|
||||
}
|
||||
this.handleKeyUpInternal(e);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Internal handler for keyup events.
|
||||
* @param {goog.events.Event} e The key event.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {
|
||||
if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
|
||||
e.keyCode == goog.events.KeyCodes.ENTER) {
|
||||
this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles an enter keypress event on fields in Gecko.
|
||||
* @param {goog.events.BrowserEvent} e The key event.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {
|
||||
// Retrieve whether the selection is collapsed before we delete it.
|
||||
var range = this.getFieldObject().getRange();
|
||||
var wasCollapsed = !range || range.isCollapsed();
|
||||
var cursorPosition = this.deleteCursorSelection_();
|
||||
|
||||
var handled = this.getFieldObject().execCommand(
|
||||
goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
|
||||
if (handled) {
|
||||
// TODO(user): I think we probably don't need to stopPropagation here
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
this.releasePositionObject_(cursorPosition);
|
||||
if (!handled) {
|
||||
this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle an enter key press in WebKit.
|
||||
* @param {goog.events.BrowserEvent} e The key press event.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =
|
||||
goog.nullFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Handle an enter key press on collapsed selection. handleEnterGecko_ ensures
|
||||
* the selection is collapsed by deleting its contents if it is not. The
|
||||
* default implementation does nothing.
|
||||
* @param {goog.events.BrowserEvent} e The key press event.
|
||||
* @param {boolean} wasCollapsed Whether the selection was collapsed before
|
||||
* the key press. If it was not, code before this function has already
|
||||
* cleared the contents of the selection.
|
||||
* @param {goog.dom.AbstractRange} range Object representing the selection.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =
|
||||
goog.nullFunction;
|
||||
|
||||
|
||||
/**
|
||||
* Names of all the nodes that we don't want to turn into block nodes in IE when
|
||||
* the user hits enter.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =
|
||||
goog.object.createSet(
|
||||
goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,
|
||||
goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,
|
||||
goog.dom.TagName.H5, goog.dom.TagName.H6);
|
||||
|
||||
|
||||
/**
|
||||
* Whether this is a node that contains a single BR tag and non-nbsp
|
||||
* whitespace.
|
||||
* @param {Node} node Node to check.
|
||||
* @return {boolean} Whether this is an element that only contains a BR.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.isBrElem = function(node) {
|
||||
return goog.editor.node.isEmpty(node) &&
|
||||
node.getElementsByTagName(goog.dom.TagName.BR).length == 1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Ensures all text in IE and Opera to be in the given tag in order to control
|
||||
* Enter spacing. Call this when Enter is pressed if desired.
|
||||
*
|
||||
* We want to make sure the user is always inside of a block (or other nodes
|
||||
* listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_). We
|
||||
* listen to keypress to force nodes that the user is leaving to turn into
|
||||
* blocks, but we also need to listen to keyup to force nodes that the user is
|
||||
* entering to turn into blocks.
|
||||
* Example: html is: "<h2>foo[cursor]</h2>", and the user hits enter. We
|
||||
* don't want to format the h2, but we do want to format the P that is
|
||||
* created on enter. The P node is not available until keyup.
|
||||
* @param {goog.dom.TagName} tag The tag name to convert to.
|
||||
* @param {boolean=} opt_keyUp Whether the function is being called on key up.
|
||||
* When called on key up, the cursor is in the newly created node, so the
|
||||
* semantics for when to change it to a block are different. Specifically,
|
||||
* if the resulting node contains only a BR, it is converted to <tag>.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(tag,
|
||||
opt_keyUp) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
var container = range.getContainer();
|
||||
var field = this.getFieldObject().getElement();
|
||||
|
||||
var paragraph;
|
||||
while (container && container != field) {
|
||||
// We don't need to ensure a block if we are already in the same block, or
|
||||
// in another block level node that we don't want to change the format of
|
||||
// (unless we're handling keyUp and that block node just contains a BR).
|
||||
var nodeName = container.nodeName;
|
||||
// Due to @bug 2455389, the call to isBrElem needs to be inlined in the if
|
||||
// instead of done before and saved in a variable, so that it can be
|
||||
// short-circuited and avoid a weird IE edge case.
|
||||
if (nodeName == tag ||
|
||||
(goog.editor.plugins.EnterHandler.
|
||||
DO_NOT_ENSURE_BLOCK_NODES_[nodeName] && !(opt_keyUp &&
|
||||
goog.editor.plugins.EnterHandler.isBrElem(container)))) {
|
||||
// Opera can create a <p> inside of a <div> in some situations,
|
||||
// such as when breaking out of a list that is contained in a <div>.
|
||||
if (goog.userAgent.OPERA && paragraph) {
|
||||
if (nodeName == tag &&
|
||||
paragraph == container.lastChild &&
|
||||
goog.editor.node.isEmpty(paragraph)) {
|
||||
goog.dom.insertSiblingAfter(paragraph, container);
|
||||
goog.dom.Range.createFromNodeContents(paragraph).select();
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&
|
||||
nodeName != tag) {
|
||||
paragraph = container;
|
||||
}
|
||||
|
||||
container = container.parentNode;
|
||||
}
|
||||
|
||||
|
||||
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {
|
||||
// IE (before IE9) has a bug where if the cursor is directly before a block
|
||||
// node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),
|
||||
// the FormatBlock command actually formats the "bar" instead of the "foo".
|
||||
// This is just wrong. To work-around this, we want to move the
|
||||
// selection back one character, and then restore it to its prior position.
|
||||
// NOTE: We use the following "range math" to detect this situation because
|
||||
// using Closure ranges here triggers a bug in IE that causes a crash.
|
||||
// parent2 != parent3 ensures moving the cursor forward one character
|
||||
// crosses at least 1 element boundary, and therefore tests if the cursor is
|
||||
// at such a boundary. The second check, parent3 != range.parentElement()
|
||||
// weeds out some cases where the elements are siblings instead of cousins.
|
||||
var needsHelp = false;
|
||||
range = range.getBrowserRangeObject();
|
||||
var range2 = range.duplicate();
|
||||
range2.moveEnd('character', 1);
|
||||
// In whitebox mode, when the cursor is at the end of the field, trying to
|
||||
// move the end of the range will do nothing, and hence the range's text
|
||||
// will be empty. In this case, the cursor clearly isn't sitting just
|
||||
// before a block node, since it isn't before anything.
|
||||
if (range2.text.length) {
|
||||
var parent2 = range2.parentElement();
|
||||
|
||||
var range3 = range2.duplicate();
|
||||
range3.collapse(false);
|
||||
var parent3 = range3.parentElement();
|
||||
|
||||
if ((needsHelp = parent2 != parent3 &&
|
||||
parent3 != range.parentElement())) {
|
||||
range.move('character', -1);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.getFieldObject().getEditableDomHelper().getDocument().execCommand(
|
||||
'FormatBlock', false, '<' + tag + '>');
|
||||
|
||||
if (needsHelp) {
|
||||
range.move('character', 1);
|
||||
range.select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the content at the current cursor position.
|
||||
* @return {Node|Object} Something representing the current cursor position.
|
||||
* See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.
|
||||
* Should be passed to releasePositionObject_ when no longer in use.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {
|
||||
return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
|
||||
this.deleteCursorSelectionW3C_() : this.deleteCursorSelectionIE_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Releases the object returned by deleteCursorSelection_.
|
||||
* @param {Node|Object} position The object returned by deleteCursorSelection_.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ =
|
||||
function(position) {
|
||||
if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {
|
||||
(/** @type {Node} */ (position)).removeNode(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Delete the selection at the current cursor position, then returns a temporary
|
||||
* node at the current position.
|
||||
* @return {Node} A temporary node marking the current cursor position. This
|
||||
* node should eventually be removed from the DOM.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =
|
||||
function() {
|
||||
var doc = this.getFieldDomHelper().getDocument();
|
||||
var range = doc.selection.createRange();
|
||||
|
||||
var id = goog.string.createUniqueString();
|
||||
range.pasteHTML('<span id="' + id + '"></span>');
|
||||
var splitNode = doc.getElementById(id);
|
||||
splitNode.id = '';
|
||||
return splitNode;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Delete the selection at the current cursor position, then returns the node
|
||||
* at the current position.
|
||||
* @return {goog.editor.range.Point} The current cursor position. Note that
|
||||
* unlike simulateEnterIE_, this should not be removed from the DOM.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =
|
||||
function() {
|
||||
var range = this.getFieldObject().getRange();
|
||||
|
||||
// Delete the current selection if it's is non-collapsed.
|
||||
// Although this is redundant in FF, it's necessary for Safari
|
||||
if (!range.isCollapsed()) {
|
||||
var shouldDelete = true;
|
||||
// Opera selects the <br> in an empty block if there is no text node
|
||||
// preceding it. To preserve inline formatting when pressing [enter] inside
|
||||
// an empty block, don't delete the selection if it only selects a <br> at
|
||||
// the end of the block.
|
||||
// TODO(user): Move this into goog.dom.Range. It should detect this state
|
||||
// when creating a range from the window selection and fix it in the created
|
||||
// range.
|
||||
if (goog.userAgent.OPERA) {
|
||||
var startNode = range.getStartNode();
|
||||
var startOffset = range.getStartOffset();
|
||||
if (startNode == range.getEndNode() &&
|
||||
// This weeds out cases where startNode is a text node.
|
||||
startNode.lastChild &&
|
||||
startNode.lastChild.tagName == goog.dom.TagName.BR &&
|
||||
// If this check is true, then endOffset is implied to be
|
||||
// startOffset + 1, because the selection is not collapsed and
|
||||
// it starts and ends within the same element.
|
||||
startOffset == startNode.childNodes.length - 1) {
|
||||
shouldDelete = false;
|
||||
}
|
||||
}
|
||||
if (shouldDelete) {
|
||||
goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
|
||||
}
|
||||
}
|
||||
|
||||
return goog.editor.range.getDeepEndPoint(range, true);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the contents of the selection from the DOM.
|
||||
* @param {goog.dom.AbstractRange} range The range to remove contents from.
|
||||
* @return {goog.dom.AbstractRange} The resulting range. Used for testing.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {
|
||||
if (range && !range.isCollapsed()) {
|
||||
var reselect = true;
|
||||
var baseNode = range.getContainerElement();
|
||||
var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);
|
||||
var rangeOffset = range.getStartOffset();
|
||||
|
||||
// Whether the selection crosses no container boundaries.
|
||||
var isInOneContainer =
|
||||
goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);
|
||||
|
||||
// Whether the selection ends in a container it doesn't fully select.
|
||||
var isPartialEnd = !isInOneContainer &&
|
||||
goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);
|
||||
|
||||
// Remove The range contents, and ensure the correct content stays selected.
|
||||
range.removeContents();
|
||||
var node = nodeOffset.findTargetNode(baseNode);
|
||||
if (node) {
|
||||
range = goog.dom.Range.createCaret(node, rangeOffset);
|
||||
} else {
|
||||
// This occurs when the node that would have been referenced has now been
|
||||
// deleted and there are no other nodes in the baseNode. Thus need to
|
||||
// set the caret to the end of the base node.
|
||||
range =
|
||||
goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);
|
||||
reselect = false;
|
||||
}
|
||||
range.select();
|
||||
|
||||
// If we just deleted everything from the container, add an nbsp
|
||||
// to the container, and leave the cursor inside of it
|
||||
if (isInOneContainer) {
|
||||
var container = goog.editor.style.getContainer(range.getStartNode());
|
||||
if (goog.editor.node.isEmpty(container, true)) {
|
||||
var html = ' ';
|
||||
if (goog.userAgent.OPERA &&
|
||||
container.tagName == goog.dom.TagName.LI) {
|
||||
// Don't break Opera's native break-out-of-lists behavior.
|
||||
html = '<br>';
|
||||
}
|
||||
goog.editor.node.replaceInnerHtml(container, html);
|
||||
goog.editor.range.selectNodeStart(container.firstChild);
|
||||
reselect = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPartialEnd) {
|
||||
/*
|
||||
This code handles the following, where | is the cursor:
|
||||
<div>a|b</div><div>c|d</div>
|
||||
After removeContents, the remaining HTML is
|
||||
<div>a</div><div>d</div>
|
||||
which means the line break between the two divs remains. This block
|
||||
moves children of the second div in to the first div to get the correct
|
||||
result:
|
||||
<div>ad</div>
|
||||
|
||||
TODO(robbyw): Should we wrap the second div's contents in a span if they
|
||||
have inline style?
|
||||
*/
|
||||
var rangeStart = goog.editor.style.getContainer(range.getStartNode());
|
||||
var redundantContainer = goog.editor.node.getNextSibling(rangeStart);
|
||||
if (rangeStart && redundantContainer) {
|
||||
goog.dom.append(rangeStart, redundantContainer.childNodes);
|
||||
goog.dom.removeNode(redundantContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (reselect) {
|
||||
// The contents of the original range are gone, so restore the cursor
|
||||
// position at the start of where the range once was.
|
||||
range = goog.dom.Range.createCaret(nodeOffset.findTargetNode(baseNode),
|
||||
rangeOffset);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the whole range is in a single block-level element.
|
||||
* @param {goog.dom.AbstractRange} range The range to check.
|
||||
* @return {boolean} Whether the whole range is in a single block-level element.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {
|
||||
// Find the block element containing the start of the selection.
|
||||
var startContainer = range.getStartNode();
|
||||
if (goog.editor.style.isContainer(startContainer)) {
|
||||
startContainer = startContainer.childNodes[range.getStartOffset()] ||
|
||||
startContainer;
|
||||
}
|
||||
startContainer = goog.editor.style.getContainer(startContainer);
|
||||
|
||||
// Find the block element containing the end of the selection.
|
||||
var endContainer = range.getEndNode();
|
||||
if (goog.editor.style.isContainer(endContainer)) {
|
||||
endContainer = endContainer.childNodes[range.getEndOffset()] ||
|
||||
endContainer;
|
||||
}
|
||||
endContainer = goog.editor.style.getContainer(endContainer);
|
||||
|
||||
// Compare the two.
|
||||
return startContainer == endContainer;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the end of the range is not at the end of a block-level
|
||||
* element.
|
||||
* @param {goog.dom.AbstractRange} range The range to check.
|
||||
* @return {boolean} Whether the end of the range is not at the end of a
|
||||
* block-level element.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {
|
||||
var endContainer = range.getEndNode();
|
||||
var endOffset = range.getEndOffset();
|
||||
var node = endContainer;
|
||||
if (goog.editor.style.isContainer(node)) {
|
||||
var child = node.childNodes[endOffset];
|
||||
// Child is null when end offset is >= length, which indicates the entire
|
||||
// container is selected. Otherwise, we also know the entire container
|
||||
// is selected if the selection ends at a new container.
|
||||
if (!child ||
|
||||
child.nodeType == goog.dom.NodeType.ELEMENT &&
|
||||
goog.editor.style.isContainer(child)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var container = goog.editor.style.getContainer(node);
|
||||
while (container != node) {
|
||||
if (goog.editor.node.getNextSibling(node)) {
|
||||
return true;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return endOffset != goog.editor.node.getLength(endContainer);
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.editor.plugins.equation.EquationBubble');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.plugins.AbstractBubblePlugin');
|
||||
goog.require('goog.string.Unicode');
|
||||
goog.require('goog.ui.editor.Bubble');
|
||||
goog.require('goog.ui.equation.ImageRenderer');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Property bubble plugin for equations.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.AbstractBubblePlugin}
|
||||
*/
|
||||
goog.editor.plugins.equation.EquationBubble = function() {
|
||||
goog.base(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.equation.EquationBubble,
|
||||
goog.editor.plugins.AbstractBubblePlugin);
|
||||
|
||||
|
||||
/**
|
||||
* Id for 'edit' link.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.equation.EquationBubble.EDIT_ID_ = 'ee_bubble_edit';
|
||||
|
||||
|
||||
/**
|
||||
* Id for 'remove' link.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.equation.EquationBubble.REMOVE_ID_ = 'ee_remove_remove';
|
||||
|
||||
|
||||
/**
|
||||
* @desc Label for the equation property bubble.
|
||||
*/
|
||||
var MSG_EE_BUBBLE_EQUATION = goog.getMsg('Equation:');
|
||||
|
||||
|
||||
/**
|
||||
* @desc Link text for equation property bubble to edit the equation.
|
||||
*/
|
||||
var MSG_EE_BUBBLE_EDIT = goog.getMsg('Edit');
|
||||
|
||||
|
||||
/**
|
||||
* @desc Link text for equation property bubble to remove the equation.
|
||||
*/
|
||||
var MSG_EE_BUBBLE_REMOVE = goog.getMsg('Remove');
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.getTrogClassId =
|
||||
function() {
|
||||
return 'EquationBubble';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.
|
||||
getBubbleTargetFromSelection = function(selectedElement) {
|
||||
if (selectedElement &&
|
||||
goog.ui.equation.ImageRenderer.isEquationElement(selectedElement)) {
|
||||
return selectedElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.createBubbleContents =
|
||||
function(bubbleContainer) {
|
||||
goog.dom.appendChild(bubbleContainer,
|
||||
bubbleContainer.ownerDocument.createTextNode(
|
||||
MSG_EE_BUBBLE_EQUATION + goog.string.Unicode.NBSP));
|
||||
|
||||
this.createLink(goog.editor.plugins.equation.EquationBubble.EDIT_ID_,
|
||||
MSG_EE_BUBBLE_EDIT, this.editEquation_, bubbleContainer);
|
||||
|
||||
goog.dom.appendChild(bubbleContainer,
|
||||
bubbleContainer.ownerDocument.createTextNode(
|
||||
MSG_EE_BUBBLE_EQUATION +
|
||||
goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING));
|
||||
|
||||
this.createLink(goog.editor.plugins.equation.EquationBubble.REMOVE_ID_,
|
||||
MSG_EE_BUBBLE_REMOVE, this.removeEquation_, bubbleContainer);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.getBubbleType =
|
||||
function() {
|
||||
return goog.dom.TagName.IMG;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.getBubbleTitle =
|
||||
function() {
|
||||
/** @desc Title for the equation bubble. */
|
||||
var MSG_EQUATION_BUBBLE_TITLE = goog.getMsg('Equation');
|
||||
return MSG_EQUATION_BUBBLE_TITLE;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes the equation associated with the bubble.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.removeEquation_ =
|
||||
function() {
|
||||
this.getFieldObject().dispatchBeforeChange();
|
||||
|
||||
goog.dom.removeNode(this.getTargetElement());
|
||||
|
||||
this.closeBubble();
|
||||
|
||||
this.getFieldObject().dispatchChange();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Opens equation editor for the equation associated with the bubble.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.equation.EquationBubble.prototype.editEquation_ =
|
||||
function() {
|
||||
var equationNode = this.getTargetElement();
|
||||
this.closeBubble();
|
||||
this.getFieldObject().execCommand(goog.editor.Command.EQUATION, equationNode);
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.editor.plugins.EquationEditorPlugin');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.plugins.AbstractDialogPlugin');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.log');
|
||||
goog.require('goog.ui.editor.AbstractDialog');
|
||||
goog.require('goog.ui.editor.EquationEditorDialog');
|
||||
goog.require('goog.ui.equation.ImageRenderer');
|
||||
goog.require('goog.ui.equation.PaletteManager');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A plugin that opens the equation editor in a dialog window.
|
||||
* @param {string=} opt_helpUrl A URL pointing to help documentation.
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.AbstractDialogPlugin}
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin = function(opt_helpUrl) {
|
||||
/**
|
||||
* The IMG element for the equation being edited, null if creating a new
|
||||
* equation.
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
this.originalElement_;
|
||||
|
||||
/**
|
||||
* A URL pointing to help documentation.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.helpUrl_ = opt_helpUrl || '';
|
||||
|
||||
/**
|
||||
* The listener key for double click events.
|
||||
* @type {goog.events.Key}
|
||||
* @private
|
||||
*/
|
||||
this.dblClickKey_;
|
||||
|
||||
goog.editor.plugins.AbstractDialogPlugin.call(this,
|
||||
goog.editor.Command.EQUATION);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.EquationEditorPlugin,
|
||||
goog.editor.plugins.AbstractDialogPlugin);
|
||||
|
||||
|
||||
/**
|
||||
* The logger for the EquationEditorPlugin.
|
||||
* @type {goog.log.Logger}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.logger_ =
|
||||
goog.log.getLogger('goog.editor.plugins.EquationEditorPlugin');
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.getTrogClassId =
|
||||
goog.functions.constant('EquationEditorPlugin');
|
||||
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.createDialog =
|
||||
function(dom, opt_arg) {
|
||||
var equationImgEl = /** @type {Element} */ (opt_arg || null);
|
||||
|
||||
var equationStr = equationImgEl ?
|
||||
goog.ui.equation.ImageRenderer.getEquationFromImage(equationImgEl) : '';
|
||||
|
||||
this.originalElement_ = equationImgEl;
|
||||
var dialog = new goog.ui.editor.EquationEditorDialog(
|
||||
this.populateContext_(dom), dom, equationStr, this.helpUrl_);
|
||||
dialog.addEventListener(goog.ui.editor.AbstractDialog.EventType.OK,
|
||||
this.handleOk_,
|
||||
false,
|
||||
this);
|
||||
return dialog;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Populates the context that this plugin runs in.
|
||||
* @param {!goog.dom.DomHelper} domHelper The dom helper to be used for the
|
||||
* palette manager.
|
||||
* @return {Object} The context that this plugin runs in.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.populateContext_ =
|
||||
function(domHelper) {
|
||||
var context = {};
|
||||
context.paletteManager = new goog.ui.equation.PaletteManager(domHelper);
|
||||
return context;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the selected text in the editable field for using as initial
|
||||
* equation string for the equation editor.
|
||||
*
|
||||
* TODO(user): Sanity check the selected text and return it only if it
|
||||
* reassembles a TeX equation and is not too long.
|
||||
*
|
||||
* @return {string} Selected text in the editable field for using it as
|
||||
* initial equation string for the equation editor.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.getEquationFromSelection_ =
|
||||
function() {
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (range) {
|
||||
return range.getText();
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.enable =
|
||||
function(fieldObject) {
|
||||
goog.base(this, 'enable', fieldObject);
|
||||
if (this.isEnabled(fieldObject)) {
|
||||
this.dblClickKey_ = goog.events.listen(fieldObject.getElement(),
|
||||
goog.events.EventType.DBLCLICK,
|
||||
goog.bind(this.handleDoubleClick_, this), false, this);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.disable =
|
||||
function(fieldObject) {
|
||||
goog.base(this, 'disable', fieldObject);
|
||||
if (!this.isEnabled(fieldObject)) {
|
||||
goog.events.unlistenByKey(this.dblClickKey_);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles double clicks in the field area.
|
||||
* @param {goog.events.Event} e The event.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.handleDoubleClick_ =
|
||||
function(e) {
|
||||
var node = /** @type {Node} */ (e.target);
|
||||
this.execCommand(goog.editor.Command.EQUATION, node);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called when user clicks OK. Inserts the equation at cursor position in the
|
||||
* active editable field.
|
||||
* @param {goog.ui.editor.EquationEditorOkEvent} e The OK event.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.EquationEditorPlugin.prototype.handleOk_ =
|
||||
function(e) {
|
||||
// First restore the selection so we can manipulate the editable field's
|
||||
// content according to what was selected.
|
||||
this.restoreOriginalSelection();
|
||||
|
||||
// Notify listeners that the editable field's contents are about to change.
|
||||
this.getFieldObject().dispatchBeforeChange();
|
||||
|
||||
var dh = this.getFieldDomHelper();
|
||||
var node = dh.htmlToDocumentFragment(e.equationHtml);
|
||||
|
||||
if (this.originalElement_) {
|
||||
// Editing existing equation: replace the old equation node with the new
|
||||
// one.
|
||||
goog.dom.replaceNode(node, this.originalElement_);
|
||||
} else {
|
||||
// Clear out what was previously selected, unless selection is already
|
||||
// empty (aka collapsed), and replace it with the new equation node.
|
||||
// TODO(user): there is a bug in FF where removeContents() may remove a
|
||||
// <br> right before and/or after the selection. Currently this is fixed
|
||||
// only for case of collapsed selection where we simply avoid calling
|
||||
// removeContants().
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (!range.isCollapsed()) {
|
||||
range.removeContents();
|
||||
}
|
||||
node = range.insertNode(node, false);
|
||||
}
|
||||
|
||||
// Place the cursor to the right of the
|
||||
// equation image.
|
||||
goog.editor.range.placeCursorNextTo(node, false);
|
||||
|
||||
this.getFieldObject().dispatchChange();
|
||||
};
|
||||
@@ -0,0 +1,326 @@
|
||||
// Copyright 2012 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 plugin to enable the First Strong Bidi algorithm. The First
|
||||
* Strong algorithm as a heuristic used to automatically set paragraph direction
|
||||
* depending on its content.
|
||||
*
|
||||
* In the documentation below, a 'paragraph' is the local element which we
|
||||
* evaluate as a whole for purposes of determining directionality. It may be a
|
||||
* block-level element (e.g. <div>) or a whole list (e.g. <ul>).
|
||||
*
|
||||
* This implementation is based on, but is not identical to, the original
|
||||
* First Strong algorithm defined in Unicode
|
||||
* @see http://www.unicode.org/reports/tr9/
|
||||
* The central difference from the original First Strong algorithm is that this
|
||||
* implementation decides the paragraph direction based on the first strong
|
||||
* character that is <em>typed</em> into the paragraph, regardless of its
|
||||
* location in the paragraph, as opposed to the original algorithm where it is
|
||||
* the first character in the paragraph <em>by location</em>, regardless of
|
||||
* whether other strong characters already appear in the paragraph, further its
|
||||
* start.
|
||||
*
|
||||
* <em>Please note</em> that this plugin does not perform the direction change
|
||||
* itself. Rather, it fires editor commands upon the key up event when a
|
||||
* direction change needs to be performed; {@code goog.editor.Command.DIR_RTL}
|
||||
* or {@code goog.editor.Command.DIR_RTL}.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.FirstStrong');
|
||||
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.TagIterator');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.i18n.bidi');
|
||||
goog.require('goog.i18n.uChar');
|
||||
goog.require('goog.iter');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* First Strong plugin.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong = function() {
|
||||
goog.base(this);
|
||||
|
||||
/**
|
||||
* Indicates whether or not the cursor is in a paragraph we have not yet
|
||||
* finished evaluating for directionality. This is set to true whenever the
|
||||
* cursor is moved, and set to false after seeing a strong character in the
|
||||
* paragraph the cursor is currently in.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isNewBlock_ = true;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the current paragraph the cursor is in should be
|
||||
* set to Right-To-Left directionality.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.switchToRtl_ = false;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the current paragraph the cursor is in should be
|
||||
* set to Left-To-Right directionality.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.switchToLtr_ = false;
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.FirstStrong, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.FirstStrong.prototype.getTrogClassId = function() {
|
||||
return 'FirstStrong';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.FirstStrong.prototype.queryCommandValue =
|
||||
function(command) {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.FirstStrong.prototype.handleSelectionChange =
|
||||
function(e, node) {
|
||||
this.isNewBlock_ = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The name of the attribute which records the input text.
|
||||
*
|
||||
* @type {string}
|
||||
* @const
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE = 'fs-input';
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.FirstStrong.prototype.handleKeyPress = function(e) {
|
||||
if (!this.isNewBlock_) {
|
||||
return false; // We've already determined this paragraph's direction.
|
||||
}
|
||||
// Ignore non-character key press events.
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
return false;
|
||||
}
|
||||
var newInput = goog.i18n.uChar.fromCharCode(e.charCode);
|
||||
|
||||
// IME's may return 0 for the charCode, which is a legitimate, non-Strong
|
||||
// charCode, or they may return an illegal charCode (for which newInput will
|
||||
// be false).
|
||||
if (!newInput || !e.charCode) {
|
||||
var browserEvent = e.getBrowserEvent();
|
||||
if (browserEvent) {
|
||||
if (goog.userAgent.IE && browserEvent['getAttribute']) {
|
||||
newInput = browserEvent['getAttribute'](
|
||||
goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE);
|
||||
} else {
|
||||
newInput = browserEvent[
|
||||
goog.editor.plugins.FirstStrong.INPUT_ATTRIBUTE];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!newInput) {
|
||||
return false; // Unrecognized key.
|
||||
}
|
||||
|
||||
var isLtr = goog.i18n.bidi.isLtrChar(newInput);
|
||||
var isRtl = !isLtr && goog.i18n.bidi.isRtlChar(newInput);
|
||||
if (!isLtr && !isRtl) {
|
||||
return false; // This character cannot change anything (it is not Strong).
|
||||
}
|
||||
// This character is Strongly LTR or Strongly RTL. We might switch direction
|
||||
// on it now, but in any case we do not need to check any more characters in
|
||||
// this paragraph after it.
|
||||
this.isNewBlock_ = false;
|
||||
|
||||
// Are there no Strong characters already in the paragraph?
|
||||
if (this.isNeutralBlock_()) {
|
||||
this.switchToRtl_ = isRtl;
|
||||
this.switchToLtr_ = isLtr;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Calls the flip directionality commands. This is done here so things go into
|
||||
* the redo-undo stack at the expected order; fist enter the input, then flip
|
||||
* directionality.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.prototype.handleKeyUp = function(e) {
|
||||
if (this.switchToRtl_) {
|
||||
var field = this.getFieldObject();
|
||||
field.dispatchChange(true);
|
||||
field.execCommand(goog.editor.Command.DIR_RTL);
|
||||
this.switchToRtl_ = false;
|
||||
} else if (this.switchToLtr_) {
|
||||
var field = this.getFieldObject();
|
||||
field.dispatchChange(true);
|
||||
field.execCommand(goog.editor.Command.DIR_LTR);
|
||||
this.switchToLtr_ = false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {Element} The lowest Block element ancestor of the node where the
|
||||
* next character will be placed.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.prototype.getBlockAncestor_ = function() {
|
||||
var start = this.getFieldObject().getRange().getStartNode();
|
||||
// Go up in the DOM until we reach a Block element.
|
||||
while (!goog.editor.plugins.FirstStrong.isBlock_(start)) {
|
||||
start = start.parentNode;
|
||||
}
|
||||
return /** @type {Element} */ (start);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the paragraph where the next character will be
|
||||
* entered contains only non-Strong characters.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.prototype.isNeutralBlock_ = function() {
|
||||
var root = this.getBlockAncestor_();
|
||||
// The exact node with the cursor location. Simply calling getStartNode() on
|
||||
// the range only returns the containing block node.
|
||||
var cursor = goog.editor.range.getDeepEndPoint(
|
||||
this.getFieldObject().getRange(), false).node;
|
||||
|
||||
// In FireFox the BR tag also represents a change in paragraph if not inside a
|
||||
// list. So we need special handling to only look at the sub-block between
|
||||
// BR elements.
|
||||
var blockFunction = (goog.userAgent.GECKO &&
|
||||
!this.isList_(root)) ?
|
||||
goog.editor.plugins.FirstStrong.isGeckoBlock_ :
|
||||
goog.editor.plugins.FirstStrong.isBlock_;
|
||||
var paragraph = this.getTextAround_(root, cursor,
|
||||
blockFunction);
|
||||
// Not using {@code goog.i18n.bidi.isNeutralText} as it contains additional,
|
||||
// unwanted checks to the content.
|
||||
return !goog.i18n.bidi.hasAnyLtr(paragraph) &&
|
||||
!goog.i18n.bidi.hasAnyRtl(paragraph);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if an element is a list element ('UL' or 'OL').
|
||||
*
|
||||
* @param {Element} element The element to test.
|
||||
* @return {boolean} Whether the element is a list element ('UL' or 'OL').
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.prototype.isList_ = function(element) {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
var tagName = element.tagName;
|
||||
return tagName == goog.dom.TagName.UL || tagName == goog.dom.TagName.OL;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the text within the local paragraph around the cursor.
|
||||
* Notice that for GECKO a BR represents a pargraph change despite not being a
|
||||
* block element.
|
||||
*
|
||||
* @param {Element} root The first block element ancestor of the node the cursor
|
||||
* is in.
|
||||
* @param {Node} cursorLocation Node where the cursor currently is, marking the
|
||||
* paragraph whose text we will return.
|
||||
* @param {function(Node): boolean} isParagraphBoundary The function to
|
||||
* determine if a node represents the start or end of the paragraph.
|
||||
* @return {string} the text in the paragraph around the cursor location.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.prototype.getTextAround_ = function(root,
|
||||
cursorLocation, isParagraphBoundary) {
|
||||
// The buffer where we're collecting the text.
|
||||
var buffer = [];
|
||||
// Have we reached the cursor yet, or are we still before it?
|
||||
var pastCursorLocation = false;
|
||||
|
||||
if (root && cursorLocation) {
|
||||
goog.iter.some(new goog.dom.TagIterator(root), function(node) {
|
||||
if (node == cursorLocation) {
|
||||
pastCursorLocation = true;
|
||||
} else if (isParagraphBoundary(node)) {
|
||||
if (pastCursorLocation) {
|
||||
// This is the end of the paragraph containing the cursor. We're done.
|
||||
return true;
|
||||
} else {
|
||||
// All we collected so far does not count; it was in a previous
|
||||
// paragraph that did not contain the cursor.
|
||||
buffer = [];
|
||||
}
|
||||
}
|
||||
if (node.nodeType == goog.dom.NodeType.TEXT) {
|
||||
buffer.push(node.nodeValue);
|
||||
}
|
||||
return false; // Keep going.
|
||||
});
|
||||
}
|
||||
return buffer.join('');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Node} node Node to check.
|
||||
* @return {boolean} Does the given node represent a Block element? Notice we do
|
||||
* not consider list items as Block elements in the algorithm.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.isBlock_ = function(node) {
|
||||
return !!node && goog.editor.node.isBlockTag(node) &&
|
||||
node.tagName != goog.dom.TagName.LI;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {Node} node Node to check.
|
||||
* @return {boolean} Does the given node represent a Block element from the
|
||||
* point of view of FireFox? Notice we do not consider list items as Block
|
||||
* elements in the algorithm.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.FirstStrong.isGeckoBlock_ = function(node) {
|
||||
return !!node && (node.tagName == goog.dom.TagName.BR ||
|
||||
goog.editor.plugins.FirstStrong.isBlock_(node));
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Handles applying header styles to text.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.HeaderFormatter');
|
||||
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Applies header styles to text.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.HeaderFormatter = function() {
|
||||
goog.editor.Plugin.call(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.HeaderFormatter, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.HeaderFormatter.prototype.getTrogClassId = function() {
|
||||
return 'HeaderFormatter';
|
||||
};
|
||||
|
||||
// TODO(user): Move execCommand functionality from basictextformatter into
|
||||
// here for headers. I'm not doing this now because it depends on the
|
||||
// switch statements in basictextformatter and we'll need to abstract that out
|
||||
// in order to seperate out any of the functions from basictextformatter.
|
||||
|
||||
|
||||
/**
|
||||
* Commands that can be passed as the optional argument to execCommand.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.editor.plugins.HeaderFormatter.HEADER_COMMAND = {
|
||||
H1: 'H1',
|
||||
H2: 'H2',
|
||||
H3: 'H3',
|
||||
H4: 'H4'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.HeaderFormatter.prototype.handleKeyboardShortcut = function(
|
||||
e, key, isModifierPressed) {
|
||||
if (!isModifierPressed) {
|
||||
return false;
|
||||
}
|
||||
var command = null;
|
||||
switch (key) {
|
||||
case '1':
|
||||
command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H1;
|
||||
break;
|
||||
case '2':
|
||||
command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H2;
|
||||
break;
|
||||
case '3':
|
||||
command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H3;
|
||||
break;
|
||||
case '4':
|
||||
command = goog.editor.plugins.HeaderFormatter.HEADER_COMMAND.H4;
|
||||
break;
|
||||
}
|
||||
if (command) {
|
||||
this.getFieldObject().execCommand(
|
||||
goog.editor.Command.FORMAT_BLOCK, command);
|
||||
// Prevent default isn't enough to cancel tab navigation in FF.
|
||||
if (goog.userAgent.GECKO) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,562 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Base class for bubble plugins.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.LinkBubble');
|
||||
goog.provide('goog.editor.plugins.LinkBubble.Action');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.editor.BrowserFeature');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.Link');
|
||||
goog.require('goog.editor.plugins.AbstractBubblePlugin');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.ui.editor.messages');
|
||||
goog.require('goog.uri.utils');
|
||||
goog.require('goog.window');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Property bubble plugin for links.
|
||||
* @param {...!goog.editor.plugins.LinkBubble.Action} var_args List of
|
||||
* extra actions supported by the bubble.
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.AbstractBubblePlugin}
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble = function(var_args) {
|
||||
goog.base(this);
|
||||
|
||||
/**
|
||||
* List of extra actions supported by the bubble.
|
||||
* @type {Array.<!goog.editor.plugins.LinkBubble.Action>}
|
||||
* @private
|
||||
*/
|
||||
this.extraActions_ = goog.array.toArray(arguments);
|
||||
|
||||
/**
|
||||
* List of spans corresponding to the extra actions.
|
||||
* @type {Array.<!Element>}
|
||||
* @private
|
||||
*/
|
||||
this.actionSpans_ = [];
|
||||
|
||||
/**
|
||||
* A list of whitelisted URL schemes which are safe to open.
|
||||
* @type {Array.<string>}
|
||||
* @private
|
||||
*/
|
||||
this.safeToOpenSchemes_ = ['http', 'https', 'ftp'];
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.LinkBubble,
|
||||
goog.editor.plugins.AbstractBubblePlugin);
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the link text.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.LINK_TEXT_ID_ = 'tr_link-text';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the test link span.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_ = 'tr_test-link-span';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the test link.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.TEST_LINK_ID_ = 'tr_test-link';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the change link span.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_ = 'tr_change-link-span';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the link.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_ = 'tr_change-link';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the delete link span.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_ = 'tr_delete-link-span';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the delete link.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.DELETE_LINK_ID_ = 'tr_delete-link';
|
||||
|
||||
|
||||
/**
|
||||
* Element id for the link bubble wrapper div.
|
||||
* type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.LINK_DIV_ID_ = 'tr_link-div';
|
||||
|
||||
|
||||
/**
|
||||
* @desc Text label for link that lets the user click it to see where the link
|
||||
* this bubble is for point to.
|
||||
*/
|
||||
var MSG_LINK_BUBBLE_TEST_LINK = goog.getMsg('Go to link: ');
|
||||
|
||||
|
||||
/**
|
||||
* @desc Label that pops up a dialog to change the link.
|
||||
*/
|
||||
var MSG_LINK_BUBBLE_CHANGE = goog.getMsg('Change');
|
||||
|
||||
|
||||
/**
|
||||
* @desc Label that allow the user to remove this link.
|
||||
*/
|
||||
var MSG_LINK_BUBBLE_REMOVE = goog.getMsg('Remove');
|
||||
|
||||
|
||||
/**
|
||||
* Whether to stop leaking the page's url via the referrer header when the
|
||||
* link text link is clicked.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Whether to block opening links with a non-whitelisted URL scheme.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.blockOpeningUnsafeSchemes_ =
|
||||
true;
|
||||
|
||||
|
||||
/**
|
||||
* Tells the plugin to stop leaking the page's url via the referrer header when
|
||||
* the link text link is clicked. When the user clicks on a link, the
|
||||
* browser makes a request for the link url, passing the url of the current page
|
||||
* in the request headers. If the user wants the current url to be kept secret
|
||||
* (e.g. an unpublished document), the owner of the url that was clicked will
|
||||
* see the secret url in the request headers, and it will no longer be a secret.
|
||||
* Calling this method will not send a referrer header in the request, just as
|
||||
* if the user had opened a blank window and typed the url in themselves.
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.stopReferrerLeaks = function() {
|
||||
// TODO(user): Right now only 2 plugins have this API to stop
|
||||
// referrer leaks. If more plugins need to do this, come up with a way to
|
||||
// enable the functionality in all plugins at once. Same thing for
|
||||
// setBlockOpeningUnsafeSchemes and associated functionality.
|
||||
this.stopReferrerLeaks_ = true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tells the plugin whether to block URLs with schemes not in the whitelist.
|
||||
* If blocking is enabled, this plugin will not linkify the link in the bubble
|
||||
* popup.
|
||||
* @param {boolean} blockOpeningUnsafeSchemes Whether to block non-whitelisted
|
||||
* schemes.
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.setBlockOpeningUnsafeSchemes =
|
||||
function(blockOpeningUnsafeSchemes) {
|
||||
this.blockOpeningUnsafeSchemes_ = blockOpeningUnsafeSchemes;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets a whitelist of allowed URL schemes that are safe to open.
|
||||
* Schemes should all be in lowercase. If the plugin is set to block opening
|
||||
* unsafe schemes, user-entered URLs will be converted to lowercase and checked
|
||||
* against this list. The whitelist has no effect if blocking is not enabled.
|
||||
* @param {Array.<string>} schemes String array of URL schemes to allow (http,
|
||||
* https, etc.).
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.setSafeToOpenSchemes =
|
||||
function(schemes) {
|
||||
this.safeToOpenSchemes_ = schemes;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.getTrogClassId = function() {
|
||||
return 'LinkBubble';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.isSupportedCommand =
|
||||
function(command) {
|
||||
return command == goog.editor.Command.UPDATE_LINK_BUBBLE;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.execCommandInternal =
|
||||
function(command, var_args) {
|
||||
if (command == goog.editor.Command.UPDATE_LINK_BUBBLE) {
|
||||
this.updateLink_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the href in the link bubble with a new link.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.updateLink_ = function() {
|
||||
var targetEl = this.getTargetElement();
|
||||
this.closeBubble();
|
||||
this.createBubble(targetEl);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.getBubbleTargetFromSelection =
|
||||
function(selectedElement) {
|
||||
var bubbleTarget = goog.dom.getAncestorByTagNameAndClass(selectedElement,
|
||||
goog.dom.TagName.A);
|
||||
|
||||
if (!bubbleTarget) {
|
||||
// See if the selection is touching the right side of a link, and if so,
|
||||
// show a bubble for that link. The check for "touching" is very brittle,
|
||||
// and currently only guarantees that it will pop up a bubble at the
|
||||
// position the cursor is placed at after the link dialog is closed.
|
||||
// NOTE(robbyw): This assumes this method is always called with
|
||||
// selected element = range.getContainerElement(). Right now this is true,
|
||||
// but attempts to re-use this method for other purposes could cause issues.
|
||||
// TODO(robbyw): Refactor this method to also take a range, and use that.
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (range && range.isCollapsed() && range.getStartOffset() == 0) {
|
||||
var startNode = range.getStartNode();
|
||||
var previous = startNode.previousSibling;
|
||||
if (previous && previous.tagName == goog.dom.TagName.A) {
|
||||
bubbleTarget = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return /** @type {Element} */ (bubbleTarget);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the optional function for getting the "test" link of a url.
|
||||
* @param {function(string) : string} func The function to use.
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.setTestLinkUrlFn = function(func) {
|
||||
this.testLinkUrlFn_ = func;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the target element url for the bubble.
|
||||
* @return {string} The url href.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.getTargetUrl = function() {
|
||||
// Get the href-attribute through getAttribute() rather than the href property
|
||||
// because Google-Toolbar on Firefox with "Send with Gmail" turned on
|
||||
// modifies the href-property of 'mailto:' links but leaves the attribute
|
||||
// untouched.
|
||||
return this.getTargetElement().getAttribute('href') || '';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.getBubbleType = function() {
|
||||
return goog.dom.TagName.A;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.getBubbleTitle = function() {
|
||||
return goog.ui.editor.messages.MSG_LINK_CAPTION;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkBubble.prototype.createBubbleContents = function(
|
||||
bubbleContainer) {
|
||||
var linkObj = this.getLinkToTextObj_();
|
||||
|
||||
// Create linkTextSpan, show plain text for e-mail address or truncate the
|
||||
// text to <= 48 characters so that property bubbles don't grow too wide and
|
||||
// create a link if URL. Only linkify valid links.
|
||||
// TODO(robbyw): Repalce this color with a CSS class.
|
||||
var color = linkObj.valid ? 'black' : 'red';
|
||||
var shouldOpenUrl = this.shouldOpenUrl(linkObj.linkText);
|
||||
var linkTextSpan;
|
||||
if (goog.editor.Link.isLikelyEmailAddress(linkObj.linkText) ||
|
||||
!linkObj.valid || !shouldOpenUrl) {
|
||||
linkTextSpan = this.dom_.createDom(goog.dom.TagName.SPAN,
|
||||
{
|
||||
id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,
|
||||
style: 'color:' + color
|
||||
}, this.dom_.createTextNode(linkObj.linkText));
|
||||
} else {
|
||||
var testMsgSpan = this.dom_.createDom(goog.dom.TagName.SPAN,
|
||||
{id: goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_},
|
||||
MSG_LINK_BUBBLE_TEST_LINK);
|
||||
linkTextSpan = this.dom_.createDom(goog.dom.TagName.SPAN,
|
||||
{
|
||||
id: goog.editor.plugins.LinkBubble.LINK_TEXT_ID_,
|
||||
style: 'color:' + color
|
||||
}, '');
|
||||
var linkText = goog.string.truncateMiddle(linkObj.linkText, 48);
|
||||
// Actually creates a pseudo-link that can't be right-clicked to open in a
|
||||
// new tab, because that would avoid the logic to stop referrer leaks.
|
||||
this.createLink(goog.editor.plugins.LinkBubble.TEST_LINK_ID_,
|
||||
this.dom_.createTextNode(linkText).data,
|
||||
this.testLink,
|
||||
linkTextSpan);
|
||||
}
|
||||
|
||||
var changeLinkSpan = this.createLinkOption(
|
||||
goog.editor.plugins.LinkBubble.CHANGE_LINK_SPAN_ID_);
|
||||
this.createLink(goog.editor.plugins.LinkBubble.CHANGE_LINK_ID_,
|
||||
MSG_LINK_BUBBLE_CHANGE, this.showLinkDialog_, changeLinkSpan);
|
||||
|
||||
// This function is called multiple times - we have to reset the array.
|
||||
this.actionSpans_ = [];
|
||||
for (var i = 0; i < this.extraActions_.length; i++) {
|
||||
var action = this.extraActions_[i];
|
||||
var actionSpan = this.createLinkOption(action.spanId_);
|
||||
this.actionSpans_.push(actionSpan);
|
||||
this.createLink(action.linkId_, action.message_,
|
||||
function() {
|
||||
action.actionFn_(this.getTargetUrl());
|
||||
},
|
||||
actionSpan);
|
||||
}
|
||||
|
||||
var removeLinkSpan = this.createLinkOption(
|
||||
goog.editor.plugins.LinkBubble.DELETE_LINK_SPAN_ID_);
|
||||
this.createLink(goog.editor.plugins.LinkBubble.DELETE_LINK_ID_,
|
||||
MSG_LINK_BUBBLE_REMOVE, this.deleteLink_, removeLinkSpan);
|
||||
|
||||
this.onShow();
|
||||
|
||||
var bubbleContents = this.dom_.createDom(goog.dom.TagName.DIV,
|
||||
{id: goog.editor.plugins.LinkBubble.LINK_DIV_ID_},
|
||||
testMsgSpan || '', linkTextSpan, changeLinkSpan);
|
||||
|
||||
for (i = 0; i < this.actionSpans_.length; i++) {
|
||||
bubbleContents.appendChild(this.actionSpans_[i]);
|
||||
}
|
||||
bubbleContents.appendChild(removeLinkSpan);
|
||||
|
||||
goog.dom.appendChild(bubbleContainer, bubbleContents);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tests the link by opening it in a new tab/window. Should be used as the
|
||||
* click event handler for the test pseudo-link.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.testLink = function() {
|
||||
goog.window.open(this.getTestLinkAction_(),
|
||||
{
|
||||
'target': '_blank',
|
||||
'noreferrer': this.stopReferrerLeaks_
|
||||
}, this.getFieldObject().getAppWindow());
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the URL should be considered invalid. This always returns
|
||||
* false in the base class, and should be overridden by subclasses that wish
|
||||
* to impose validity rules on URLs.
|
||||
* @param {string} url The url to check.
|
||||
* @return {boolean} Whether the URL should be considered invalid.
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.isInvalidUrl = goog.functions.FALSE;
|
||||
|
||||
|
||||
/**
|
||||
* Gets the text to display for a link, based on the type of link
|
||||
* @return {Object} Returns an object of the form:
|
||||
* {linkText: displayTextForLinkTarget, valid: ifTheLinkIsValid}.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.getLinkToTextObj_ = function() {
|
||||
var isError;
|
||||
var targetUrl = this.getTargetUrl();
|
||||
|
||||
if (this.isInvalidUrl(targetUrl)) {
|
||||
/**
|
||||
* @desc Message shown in a link bubble when the link is not a valid url.
|
||||
*/
|
||||
var MSG_INVALID_URL_LINK_BUBBLE = goog.getMsg('invalid url');
|
||||
targetUrl = MSG_INVALID_URL_LINK_BUBBLE;
|
||||
isError = true;
|
||||
} else if (goog.editor.Link.isMailto(targetUrl)) {
|
||||
targetUrl = targetUrl.substring(7); // 7 == "mailto:".length
|
||||
}
|
||||
|
||||
return {linkText: targetUrl, valid: !isError};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Shows the link dialog.
|
||||
* @param {goog.events.BrowserEvent} e The event.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.showLinkDialog_ = function(e) {
|
||||
// Needed when this occurs due to an ENTER key event, else the newly created
|
||||
// dialog manages to have its OK button pressed, causing it to disappear.
|
||||
e.preventDefault();
|
||||
|
||||
this.getFieldObject().execCommand(goog.editor.Command.MODAL_LINK_EDITOR,
|
||||
new goog.editor.Link(
|
||||
/** @type {HTMLAnchorElement} */ (this.getTargetElement()),
|
||||
false));
|
||||
this.closeBubble();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deletes the link associated with the bubble
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.deleteLink_ = function() {
|
||||
this.getFieldObject().dispatchBeforeChange();
|
||||
|
||||
var link = this.getTargetElement();
|
||||
var child = link.lastChild;
|
||||
goog.dom.flattenElement(link);
|
||||
goog.editor.range.placeCursorNextTo(child, false);
|
||||
|
||||
this.closeBubble();
|
||||
|
||||
this.getFieldObject().dispatchChange();
|
||||
this.getFieldObject().focus();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the proper state for the action links.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.onShow = function() {
|
||||
var linkDiv = this.dom_.getElement(
|
||||
goog.editor.plugins.LinkBubble.LINK_DIV_ID_);
|
||||
if (linkDiv) {
|
||||
var testLinkSpan = this.dom_.getElement(
|
||||
goog.editor.plugins.LinkBubble.TEST_LINK_SPAN_ID_);
|
||||
if (testLinkSpan) {
|
||||
var url = this.getTargetUrl();
|
||||
goog.style.setElementShown(testLinkSpan, !goog.editor.Link.isMailto(url));
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.extraActions_.length; i++) {
|
||||
var action = this.extraActions_[i];
|
||||
var actionSpan = this.dom_.getElement(action.spanId_);
|
||||
if (actionSpan) {
|
||||
goog.style.setElementShown(actionSpan, action.toShowFn_(
|
||||
this.getTargetUrl()));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the url for the bubble test link. The test link is the link in the
|
||||
* bubble the user can click on to make sure the link they entered is correct.
|
||||
* @return {string} The url for the bubble link href.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.getTestLinkAction_ = function() {
|
||||
var targetUrl = this.getTargetUrl();
|
||||
return this.testLinkUrlFn_ ? this.testLinkUrlFn_(targetUrl) : targetUrl;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the plugin should open the given url in a new window.
|
||||
* @param {string} url The url to check.
|
||||
* @return {boolean} If the plugin should open the given url in a new window.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.shouldOpenUrl = function(url) {
|
||||
return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether or not a url has a scheme which is safe to open.
|
||||
* Schemes like javascript are unsafe due to the possibility of XSS.
|
||||
* @param {string} url A url.
|
||||
* @return {boolean} Whether the url has a safe scheme.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.prototype.isSafeSchemeToOpen_ =
|
||||
function(url) {
|
||||
var scheme = goog.uri.utils.getScheme(url) || 'http';
|
||||
return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructor for extra actions that can be added to the link bubble.
|
||||
* @param {string} spanId The ID for the span showing the action.
|
||||
* @param {string} linkId The ID for the link showing the action.
|
||||
* @param {string} message The text for the link showing the action.
|
||||
* @param {function(string):boolean} toShowFn Test function to determine whether
|
||||
* to show the action for the given URL.
|
||||
* @param {function(string):void} actionFn Action function to run when the
|
||||
* action is clicked. Takes the current target URL as a parameter.
|
||||
* @constructor
|
||||
*/
|
||||
goog.editor.plugins.LinkBubble.Action = function(spanId, linkId, message,
|
||||
toShowFn, actionFn) {
|
||||
this.spanId_ = spanId;
|
||||
this.linkId_ = linkId;
|
||||
this.message_ = message;
|
||||
this.toShowFn_ = toShowFn;
|
||||
this.actionFn_ = actionFn;
|
||||
};
|
||||
@@ -0,0 +1,437 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview A plugin for the LinkDialog.
|
||||
*
|
||||
* @author nicksantos@google.com (Nick Santos)
|
||||
* @author marcosalmeida@google.com (Marcos Almeida)
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.LinkDialogPlugin');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.plugins.AbstractDialogPlugin');
|
||||
goog.require('goog.events.EventHandler');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.ui.editor.AbstractDialog.EventType');
|
||||
goog.require('goog.ui.editor.LinkDialog');
|
||||
goog.require('goog.ui.editor.LinkDialog.EventType');
|
||||
goog.require('goog.ui.editor.LinkDialog.OkEvent');
|
||||
goog.require('goog.uri.utils');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A plugin that opens the link dialog.
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.AbstractDialogPlugin}
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin = function() {
|
||||
goog.base(this, goog.editor.Command.MODAL_LINK_EDITOR);
|
||||
|
||||
/**
|
||||
* Event handler for this object.
|
||||
* @type {goog.events.EventHandler}
|
||||
* @private
|
||||
*/
|
||||
this.eventHandler_ = new goog.events.EventHandler(this);
|
||||
|
||||
|
||||
/**
|
||||
* A list of whitelisted URL schemes which are safe to open.
|
||||
* @type {Array.<string>}
|
||||
* @private
|
||||
*/
|
||||
this.safeToOpenSchemes_ = ['http', 'https', 'ftp'];
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.LinkDialogPlugin,
|
||||
goog.editor.plugins.AbstractDialogPlugin);
|
||||
|
||||
|
||||
/**
|
||||
* Link object that the dialog is editing.
|
||||
* @type {goog.editor.Link}
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.currentLink_;
|
||||
|
||||
|
||||
/**
|
||||
* Optional warning to show about email addresses.
|
||||
* @type {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.emailWarning_;
|
||||
|
||||
|
||||
/**
|
||||
* Whether to show a checkbox where the user can choose to have the link open in
|
||||
* a new window.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.showOpenLinkInNewWindow_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the "open link in new window" checkbox should be checked when the
|
||||
* dialog is shown, and also whether it was checked last time the dialog was
|
||||
* closed.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.isOpenLinkInNewWindowChecked_ =
|
||||
false;
|
||||
|
||||
|
||||
/**
|
||||
* Weather to show a checkbox where the user can choose to add 'rel=nofollow'
|
||||
* attribute added to the link.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.showRelNoFollow_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Whether to stop referrer leaks. Defaults to false.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.stopReferrerLeaks_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Whether to block opening links with a non-whitelisted URL scheme.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.blockOpeningUnsafeSchemes_ =
|
||||
true;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.getTrogClassId =
|
||||
goog.functions.constant('LinkDialogPlugin');
|
||||
|
||||
|
||||
/**
|
||||
* Tells the plugin whether to block URLs with schemes not in the whitelist.
|
||||
* If blocking is enabled, this plugin will stop the 'Test Link' popup
|
||||
* window from being created. Blocking doesn't affect link creation--if the
|
||||
* user clicks the 'OK' button with an unsafe URL, the link will still be
|
||||
* created as normal.
|
||||
* @param {boolean} blockOpeningUnsafeSchemes Whether to block non-whitelisted
|
||||
* schemes.
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.setBlockOpeningUnsafeSchemes =
|
||||
function(blockOpeningUnsafeSchemes) {
|
||||
this.blockOpeningUnsafeSchemes_ = blockOpeningUnsafeSchemes;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets a whitelist of allowed URL schemes that are safe to open.
|
||||
* Schemes should all be in lowercase. If the plugin is set to block opening
|
||||
* unsafe schemes, user-entered URLs will be converted to lowercase and checked
|
||||
* against this list. The whitelist has no effect if blocking is not enabled.
|
||||
* @param {Array.<string>} schemes String array of URL schemes to allow (http,
|
||||
* https, etc.).
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.setSafeToOpenSchemes =
|
||||
function(schemes) {
|
||||
this.safeToOpenSchemes_ = schemes;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tells the dialog to show a checkbox where the user can choose to have the
|
||||
* link open in a new window.
|
||||
* @param {boolean} startChecked Whether to check the checkbox the first
|
||||
* time the dialog is shown. Subesquent times the checkbox will remember its
|
||||
* previous state.
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.showOpenLinkInNewWindow =
|
||||
function(startChecked) {
|
||||
this.showOpenLinkInNewWindow_ = true;
|
||||
this.isOpenLinkInNewWindowChecked_ = startChecked;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tells the dialog to show a checkbox where the user can choose to have
|
||||
* 'rel=nofollow' attribute added to the link.
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.showRelNoFollow = function() {
|
||||
this.showRelNoFollow_ = true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the"open link in new window" checkbox was checked last time
|
||||
* the dialog was closed.
|
||||
* @return {boolean} Whether the"open link in new window" checkbox was checked
|
||||
* last time the dialog was closed.
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.
|
||||
getOpenLinkInNewWindowCheckedState = function() {
|
||||
return this.isOpenLinkInNewWindowChecked_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Tells the plugin to stop leaking the page's url via the referrer header when
|
||||
* the "test this link" link is clicked. When the user clicks on a link, the
|
||||
* browser makes a request for the link url, passing the url of the current page
|
||||
* in the request headers. If the user wants the current url to be kept secret
|
||||
* (e.g. an unpublished document), the owner of the url that was clicked will
|
||||
* see the secret url in the request headers, and it will no longer be a secret.
|
||||
* Calling this method will not send a referrer header in the request, just as
|
||||
* if the user had opened a blank window and typed the url in themselves.
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.stopReferrerLeaks = function() {
|
||||
this.stopReferrerLeaks_ = true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the warning message to show to users about including email addresses on
|
||||
* public web pages.
|
||||
* @param {string} emailWarning Warning message to show users about including
|
||||
* email addresses on the web.
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.setEmailWarning = function(
|
||||
emailWarning) {
|
||||
this.emailWarning_ = emailWarning;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles execCommand by opening the dialog.
|
||||
* @param {string} command The command to execute.
|
||||
* @param {*=} opt_arg {@link A goog.editor.Link} object representing the link
|
||||
* being edited.
|
||||
* @return {*} Always returns true, indicating the dialog was shown.
|
||||
* @protected
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.execCommandInternal = function(
|
||||
command, opt_arg) {
|
||||
this.currentLink_ = /** @type {goog.editor.Link} */(opt_arg);
|
||||
return goog.base(this, 'execCommandInternal', command, opt_arg);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles when the dialog closes.
|
||||
* @param {goog.events.Event} e The AFTER_HIDE event object.
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.handleAfterHide = function(e) {
|
||||
goog.base(this, 'handleAfterHide', e);
|
||||
this.currentLink_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.events.EventHandler} The event handler.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.getEventHandler = function() {
|
||||
return this.eventHandler_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.editor.Link} The link being edited.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.getCurrentLink = function() {
|
||||
return this.currentLink_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new instance of the dialog and registers for the relevant events.
|
||||
* @param {goog.dom.DomHelper} dialogDomHelper The dom helper to be used to
|
||||
* create the dialog.
|
||||
* @param {*=} opt_link The target link (should be a goog.editor.Link).
|
||||
* @return {goog.ui.editor.LinkDialog} The dialog.
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.createDialog = function(
|
||||
dialogDomHelper, opt_link) {
|
||||
var dialog = new goog.ui.editor.LinkDialog(dialogDomHelper,
|
||||
/** @type {goog.editor.Link} */ (opt_link));
|
||||
if (this.emailWarning_) {
|
||||
dialog.setEmailWarning(this.emailWarning_);
|
||||
}
|
||||
if (this.showOpenLinkInNewWindow_) {
|
||||
dialog.showOpenLinkInNewWindow(this.isOpenLinkInNewWindowChecked_);
|
||||
}
|
||||
if (this.showRelNoFollow_) {
|
||||
dialog.showRelNoFollow();
|
||||
}
|
||||
dialog.setStopReferrerLeaks(this.stopReferrerLeaks_);
|
||||
this.eventHandler_.
|
||||
listen(dialog, goog.ui.editor.AbstractDialog.EventType.OK,
|
||||
this.handleOk).
|
||||
listen(dialog, goog.ui.editor.AbstractDialog.EventType.CANCEL,
|
||||
this.handleCancel_).
|
||||
listen(dialog, goog.ui.editor.LinkDialog.EventType.BEFORE_TEST_LINK,
|
||||
this.handleBeforeTestLink);
|
||||
return dialog;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.disposeInternal = function() {
|
||||
goog.base(this, 'disposeInternal');
|
||||
this.eventHandler_.dispose();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the OK event from the dialog by updating the link in the field.
|
||||
* @param {goog.ui.editor.LinkDialog.OkEvent} e OK event object.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.handleOk = function(e) {
|
||||
// We're not restoring the original selection, so clear it out.
|
||||
this.disposeOriginalSelection();
|
||||
|
||||
this.currentLink_.setTextAndUrl(e.linkText, e.linkUrl);
|
||||
if (this.showOpenLinkInNewWindow_) {
|
||||
// Save checkbox state for next time.
|
||||
this.isOpenLinkInNewWindowChecked_ = e.openInNewWindow;
|
||||
}
|
||||
|
||||
var anchor = this.currentLink_.getAnchor();
|
||||
this.touchUpAnchorOnOk_(anchor, e);
|
||||
var extraAnchors = this.currentLink_.getExtraAnchors();
|
||||
for (var i = 0; i < extraAnchors.length; ++i) {
|
||||
extraAnchors[i].href = anchor.href;
|
||||
this.touchUpAnchorOnOk_(extraAnchors[i], e);
|
||||
}
|
||||
|
||||
// Place cursor to the right of the modified link.
|
||||
this.currentLink_.placeCursorRightOf();
|
||||
|
||||
this.getFieldObject().focus();
|
||||
|
||||
this.getFieldObject().dispatchSelectionChangeEvent();
|
||||
this.getFieldObject().dispatchChange();
|
||||
|
||||
this.eventHandler_.removeAll();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Apply the necessary properties to a link upon Ok being clicked in the dialog.
|
||||
* @param {HTMLAnchorElement} anchor The anchor to set properties on.
|
||||
* @param {goog.events.Event} e Event object.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.touchUpAnchorOnOk_ =
|
||||
function(anchor, e) {
|
||||
if (this.showOpenLinkInNewWindow_) {
|
||||
if (e.openInNewWindow) {
|
||||
anchor.target = '_blank';
|
||||
} else {
|
||||
if (anchor.target == '_blank') {
|
||||
anchor.target = '';
|
||||
}
|
||||
// If user didn't indicate to open in a new window but the link already
|
||||
// had a target other than '_blank', let's leave what they had before.
|
||||
}
|
||||
}
|
||||
|
||||
if (this.showRelNoFollow_) {
|
||||
var alreadyPresent = goog.ui.editor.LinkDialog.hasNoFollow(anchor.rel);
|
||||
if (alreadyPresent && !e.noFollow) {
|
||||
anchor.rel = goog.ui.editor.LinkDialog.removeNoFollow(anchor.rel);
|
||||
} else if (!alreadyPresent && e.noFollow) {
|
||||
anchor.rel = anchor.rel ? anchor.rel + ' nofollow' : 'nofollow';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the CANCEL event from the dialog by clearing the anchor if needed.
|
||||
* @param {goog.events.Event} e Event object.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.handleCancel_ = function(e) {
|
||||
if (this.currentLink_.isNew()) {
|
||||
goog.dom.flattenElement(this.currentLink_.getAnchor());
|
||||
var extraAnchors = this.currentLink_.getExtraAnchors();
|
||||
for (var i = 0; i < extraAnchors.length; ++i) {
|
||||
goog.dom.flattenElement(extraAnchors[i]);
|
||||
}
|
||||
// Make sure listeners know the anchor was flattened out.
|
||||
this.getFieldObject().dispatchChange();
|
||||
}
|
||||
|
||||
this.eventHandler_.removeAll();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the BeforeTestLink event fired when the 'test' link is clicked.
|
||||
* @param {goog.ui.editor.LinkDialog.BeforeTestLinkEvent} e BeforeTestLink event
|
||||
* object.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.handleBeforeTestLink =
|
||||
function(e) {
|
||||
if (!this.shouldOpenUrl(e.url)) {
|
||||
/** @desc Message when the user tries to test (preview) a link, but the
|
||||
* link cannot be tested. */
|
||||
var MSG_UNSAFE_LINK = goog.getMsg('This link cannot be tested.');
|
||||
alert(MSG_UNSAFE_LINK);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the plugin should open the given url in a new window.
|
||||
* @param {string} url The url to check.
|
||||
* @return {boolean} If the plugin should open the given url in a new window.
|
||||
* @protected
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.shouldOpenUrl = function(url) {
|
||||
return !this.blockOpeningUnsafeSchemes_ || this.isSafeSchemeToOpen_(url);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether or not a url has a scheme which is safe to open.
|
||||
* Schemes like javascript are unsafe due to the possibility of XSS.
|
||||
* @param {string} url A url.
|
||||
* @return {boolean} Whether the url has a safe scheme.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LinkDialogPlugin.prototype.isSafeSchemeToOpen_ =
|
||||
function(url) {
|
||||
var scheme = goog.uri.utils.getScheme(url) || 'http';
|
||||
return goog.array.contains(this.safeToOpenSchemes_, scheme.toLowerCase());
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2011 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 Adds a keyboard shortcut for the link command.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.LinkShortcutPlugin');
|
||||
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.Link');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.string');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to add a keyboard shortcut for the link command
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.LinkShortcutPlugin = function() {
|
||||
goog.base(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.LinkShortcutPlugin, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LinkShortcutPlugin.prototype.getTrogClassId = function() {
|
||||
return 'LinkShortcutPlugin';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.LinkShortcutPlugin.prototype.handleKeyboardShortcut =
|
||||
function(e, key, isModifierPressed) {
|
||||
var command;
|
||||
if (isModifierPressed && key == 'k' && !e.shiftKey) {
|
||||
var link = /** @type {goog.editor.Link?} */ (
|
||||
this.getFieldObject().execCommand(goog.editor.Command.LINK));
|
||||
if (link) {
|
||||
link.finishLinkCreation(this.getFieldObject());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Editor plugin to handle tab keys in lists to indent and
|
||||
* outdent.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author ajp@google.com (Andy Perelson)
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.ListTabHandler');
|
||||
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.plugins.AbstractTabHandler');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to handle tab keys in lists to indent and outdent.
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.AbstractTabHandler}
|
||||
*/
|
||||
goog.editor.plugins.ListTabHandler = function() {
|
||||
goog.editor.plugins.AbstractTabHandler.call(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.ListTabHandler,
|
||||
goog.editor.plugins.AbstractTabHandler);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.ListTabHandler.prototype.getTrogClassId = function() {
|
||||
return 'ListTabHandler';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.ListTabHandler.prototype.handleTabKey = function(e) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (goog.dom.getAncestorByTagNameAndClass(range.getContainerElement(),
|
||||
goog.dom.TagName.LI) ||
|
||||
goog.iter.some(range, function(node) {
|
||||
return node.tagName == goog.dom.TagName.LI;
|
||||
})) {
|
||||
this.getFieldObject().execCommand(e.shiftKey ?
|
||||
goog.editor.Command.OUTDENT :
|
||||
goog.editor.Command.INDENT);
|
||||
e.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview A plugin that fills the field with lorem ipsum text when it's
|
||||
* empty and does not have the focus. Applies to both editable and uneditable
|
||||
* fields.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.LoremIpsum');
|
||||
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.functions');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A plugin that manages lorem ipsum state of editable fields.
|
||||
* @param {string} message The lorem ipsum message.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.LoremIpsum = function(message) {
|
||||
goog.editor.Plugin.call(this);
|
||||
|
||||
/**
|
||||
* The lorem ipsum message.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.message_ = message;
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.LoremIpsum, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LoremIpsum.prototype.getTrogClassId =
|
||||
goog.functions.constant('LoremIpsum');
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LoremIpsum.prototype.activeOnUneditableFields =
|
||||
goog.functions.TRUE;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the field is currently filled with lorem ipsum text.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LoremIpsum.prototype.usingLorem_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Handles queryCommandValue.
|
||||
* @param {string} command The command to query.
|
||||
* @return {boolean} The result.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.LoremIpsum.prototype.queryCommandValue = function(command) {
|
||||
return command == goog.editor.Command.USING_LOREM && this.usingLorem_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles execCommand.
|
||||
* @param {string} command The command to execute.
|
||||
* Should be CLEAR_LOREM or UPDATE_LOREM.
|
||||
* @param {*=} opt_placeCursor Whether to place the cursor in the field
|
||||
* after clearing lorem. Should be a boolean.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.LoremIpsum.prototype.execCommand = function(command,
|
||||
opt_placeCursor) {
|
||||
if (command == goog.editor.Command.CLEAR_LOREM) {
|
||||
this.clearLorem_(!!opt_placeCursor);
|
||||
} else if (command == goog.editor.Command.UPDATE_LOREM) {
|
||||
this.updateLorem_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.LoremIpsum.prototype.isSupportedCommand =
|
||||
function(command) {
|
||||
return command == goog.editor.Command.CLEAR_LOREM ||
|
||||
command == goog.editor.Command.UPDATE_LOREM ||
|
||||
command == goog.editor.Command.USING_LOREM;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the lorem ipsum text in a goog.editor.Field if needed.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LoremIpsum.prototype.updateLorem_ = function() {
|
||||
// Try to apply lorem ipsum if:
|
||||
// 1) We have lorem ipsum text
|
||||
// 2) There's not a dialog open, as that screws
|
||||
// with the dialog's ability to properly restore the selection
|
||||
// on dialog close (since the DOM nodes would get clobbered in FF)
|
||||
// 3) We're not using lorem already
|
||||
// 4) The field is not currently active (doesn't have focus).
|
||||
var fieldObj = this.getFieldObject();
|
||||
if (!this.usingLorem_ &&
|
||||
!fieldObj.inModalMode() &&
|
||||
goog.editor.Field.getActiveFieldId() != fieldObj.id) {
|
||||
var field = fieldObj.getElement();
|
||||
if (!field) {
|
||||
// Fallback on the original element. This is needed by
|
||||
// fields managed by click-to-edit.
|
||||
field = fieldObj.getOriginalElement();
|
||||
}
|
||||
|
||||
goog.asserts.assert(field);
|
||||
if (goog.editor.node.isEmpty(field)) {
|
||||
this.usingLorem_ = true;
|
||||
|
||||
// Save the old font style so it can be restored when we
|
||||
// clear the lorem ipsum style.
|
||||
this.oldFontStyle_ = field.style.fontStyle;
|
||||
field.style.fontStyle = 'italic';
|
||||
fieldObj.setHtml(true, this.message_, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clear an EditableField's lorem ipsum and put in initial text if needed.
|
||||
*
|
||||
* If using click-to-edit mode (where Trogedit manages whether the field
|
||||
* is editable), this works for both editable and uneditable fields.
|
||||
*
|
||||
* TODO(user): Is this really necessary? See TODO below.
|
||||
* @param {boolean=} opt_placeCursor Whether to place the cursor in the field
|
||||
* after clearing lorem.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.LoremIpsum.prototype.clearLorem_ = function(
|
||||
opt_placeCursor) {
|
||||
// Don't mess with lorem state when a dialog is open as that screws
|
||||
// with the dialog's ability to properly restore the selection
|
||||
// on dialog close (since the DOM nodes would get clobbered)
|
||||
var fieldObj = this.getFieldObject();
|
||||
if (this.usingLorem_ && !fieldObj.inModalMode()) {
|
||||
var field = fieldObj.getElement();
|
||||
if (!field) {
|
||||
// Fallback on the original element. This is needed by
|
||||
// fields managed by click-to-edit.
|
||||
field = fieldObj.getOriginalElement();
|
||||
}
|
||||
|
||||
goog.asserts.assert(field);
|
||||
this.usingLorem_ = false;
|
||||
field.style.fontStyle = this.oldFontStyle_;
|
||||
fieldObj.setHtml(true, null, true);
|
||||
|
||||
// TODO(nicksantos): I'm pretty sure that this is a hack, but talk to
|
||||
// Julie about why this is necessary and what to do with it. Really,
|
||||
// we need to figure out where it's necessary and remove it where it's
|
||||
// not. Safari never places the cursor on its own willpower.
|
||||
if (opt_placeCursor && fieldObj.isLoaded()) {
|
||||
if (goog.userAgent.WEBKIT) {
|
||||
goog.dom.getOwnerDocument(fieldObj.getElement()).body.focus();
|
||||
fieldObj.focusAndPlaceCursorAtStart();
|
||||
} else if (goog.userAgent.OPERA) {
|
||||
fieldObj.placeCursorAtStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,779 @@
|
||||
// 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 Plugin to handle Remove Formatting.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.RemoveFormatting');
|
||||
|
||||
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.Plugin');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A plugin to handle removing formatting from selected text.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting = function() {
|
||||
goog.editor.Plugin.call(this);
|
||||
|
||||
/**
|
||||
* Optional function to perform remove formatting in place of the
|
||||
* provided removeFormattingWorker_.
|
||||
* @type {?function(string): string}
|
||||
* @private
|
||||
*/
|
||||
this.optRemoveFormattingFunc_ = null;
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.RemoveFormatting, goog.editor.Plugin);
|
||||
|
||||
|
||||
/**
|
||||
* The editor command this plugin in handling.
|
||||
* @type {string}
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND =
|
||||
'+removeFormat';
|
||||
|
||||
|
||||
/**
|
||||
* Regular expression that matches a block tag name.
|
||||
* @type {RegExp}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.BLOCK_RE_ =
|
||||
/^(DIV|TR|LI|BLOCKQUOTE|H\d|PRE|XMP)/;
|
||||
|
||||
|
||||
/**
|
||||
* Appends a new line to a string buffer.
|
||||
* @param {Array.<string>} sb The string buffer to add to.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_ = function(sb) {
|
||||
sb.push('<br>');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Create a new range delimited by the start point of the first range and
|
||||
* the end point of the second range.
|
||||
* @param {goog.dom.AbstractRange} startRange Use the start point of this
|
||||
* range as the beginning of the new range.
|
||||
* @param {goog.dom.AbstractRange} endRange Use the end point of this
|
||||
* range as the end of the new range.
|
||||
* @return {goog.dom.AbstractRange} The new range.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_ = function(
|
||||
startRange, endRange) {
|
||||
return goog.dom.Range.createFromNodes(
|
||||
startRange.getStartNode(), startRange.getStartOffset(),
|
||||
endRange.getEndNode(), endRange.getEndOffset());
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.RemoveFormatting.prototype.getTrogClassId = function() {
|
||||
return 'RemoveFormatting';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.RemoveFormatting.prototype.isSupportedCommand = function(
|
||||
command) {
|
||||
return command ==
|
||||
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.RemoveFormatting.prototype.execCommandInternal =
|
||||
function(command, var_args) {
|
||||
if (command ==
|
||||
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND) {
|
||||
this.removeFormatting_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.RemoveFormatting.prototype.handleKeyboardShortcut =
|
||||
function(e, key, isModifierPressed) {
|
||||
if (!isModifierPressed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key == ' ') {
|
||||
this.getFieldObject().execCommand(
|
||||
goog.editor.plugins.RemoveFormatting.REMOVE_FORMATTING_COMMAND);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes formatting from the current selection. Removes basic formatting
|
||||
* (B/I/U) using the browser's execCommand. Then extracts the html from the
|
||||
* selection to convert, calls either a client's specified removeFormattingFunc
|
||||
* callback or trogedit's general built-in removeFormattingWorker_,
|
||||
* and then replaces the current selection with the converted text.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.removeFormatting_ = function() {
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (range.isCollapsed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the html to format and send it off for formatting. Built in
|
||||
// removeFormat only strips some inline elements and some inline CSS styles
|
||||
var convFunc = this.optRemoveFormattingFunc_ ||
|
||||
goog.bind(this.removeFormattingWorker_, this);
|
||||
this.convertSelectedHtmlText_(convFunc);
|
||||
|
||||
// Do the execCommand last as it needs block elements removed to work
|
||||
// properly on background/fontColor in FF. There are, unfortunately, still
|
||||
// cases where background/fontColor are not removed here.
|
||||
var doc = this.getFieldDomHelper().getDocument();
|
||||
doc.execCommand('RemoveFormat', false, undefined);
|
||||
|
||||
if (goog.editor.BrowserFeature.ADDS_NBSPS_IN_REMOVE_FORMAT) {
|
||||
// WebKit converts spaces to non-breaking spaces when doing a RemoveFormat.
|
||||
// See: https://bugs.webkit.org/show_bug.cgi?id=14062
|
||||
this.convertSelectedHtmlText_(function(text) {
|
||||
// This loses anything that might have legitimately been a non-breaking
|
||||
// space, but that's better than the alternative of only having non-
|
||||
// breaking spaces.
|
||||
// Old versions of WebKit (Safari 3, Chrome 1) incorrectly match /u00A0
|
||||
// and newer versions properly match .
|
||||
var nbspRegExp =
|
||||
goog.userAgent.isVersionOrHigher('528') ? / /g : /\u00A0/g;
|
||||
return text.replace(nbspRegExp, ' ');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Finds the nearest ancestor of the node that is a table.
|
||||
* @param {Node} nodeToCheck Node to search from.
|
||||
* @return {Node} The table, or null if one was not found.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.getTableAncestor_ = function(
|
||||
nodeToCheck) {
|
||||
var fieldElement = this.getFieldObject().getElement();
|
||||
while (nodeToCheck && nodeToCheck != fieldElement) {
|
||||
if (nodeToCheck.tagName == goog.dom.TagName.TABLE) {
|
||||
return nodeToCheck;
|
||||
}
|
||||
nodeToCheck = nodeToCheck.parentNode;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replaces the contents of the selection with html. Does its best to maintain
|
||||
* the original selection. Also does its best to result in a valid DOM.
|
||||
*
|
||||
* TODO(user): See if there's any way to make this work on Ranges, and then
|
||||
* move it into goog.editor.range. The Firefox implementation uses execCommand
|
||||
* on the document, so must work on the actual selection.
|
||||
*
|
||||
* @param {string} html The html string to insert into the range.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.pasteHtml_ = function(html) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
|
||||
var dh = this.getFieldDomHelper();
|
||||
// Use markers to set the extent of the selection so that we can reselect it
|
||||
// afterwards. This works better than builtin range manipulation in FF and IE
|
||||
// because their implementations are so self-inconsistent and buggy.
|
||||
var startSpanId = goog.string.createUniqueString();
|
||||
var endSpanId = goog.string.createUniqueString();
|
||||
html = '<span id="' + startSpanId + '"></span>' + html +
|
||||
'<span id="' + endSpanId + '"></span>';
|
||||
var dummyNodeId = goog.string.createUniqueString();
|
||||
var dummySpanText = '<span id="' + dummyNodeId + '"></span>';
|
||||
|
||||
if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
|
||||
// IE's selection often doesn't include the outermost tags.
|
||||
// We want to use pasteHTML to replace the range contents with the newly
|
||||
// unformatted text, so we have to check to make sure we aren't just
|
||||
// pasting into some stray tags. To do this, we first clear out the
|
||||
// contents of the range and then delete all empty nodes parenting the now
|
||||
// empty range. This way, the pasted contents are never re-embedded into
|
||||
// formated nodes. Pasting purely empty html does not work, since IE moves
|
||||
// the selection inside the next node, so we insert a dummy span.
|
||||
var textRange = range.getTextRange(0).getBrowserRangeObject();
|
||||
textRange.pasteHTML(dummySpanText);
|
||||
var parent;
|
||||
while ((parent = textRange.parentElement()) &&
|
||||
goog.editor.node.isEmpty(parent) &&
|
||||
!goog.editor.node.isEditableContainer(parent)) {
|
||||
var tag = parent.nodeName;
|
||||
// We can't remove these table tags as it will invalidate the table dom.
|
||||
if (tag == goog.dom.TagName.TD ||
|
||||
tag == goog.dom.TagName.TR ||
|
||||
tag == goog.dom.TagName.TH) {
|
||||
break;
|
||||
}
|
||||
|
||||
goog.dom.removeNode(parent);
|
||||
}
|
||||
textRange.pasteHTML(html);
|
||||
var dummySpan = dh.getElement(dummyNodeId);
|
||||
// If we entered the while loop above, the node has already been removed
|
||||
// since it was a child of parent and parent was removed.
|
||||
if (dummySpan) {
|
||||
goog.dom.removeNode(dummySpan);
|
||||
}
|
||||
} else if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
|
||||
// insertHtml and range.insertNode don't merge blocks correctly.
|
||||
// (e.g. if your selection spans two paragraphs)
|
||||
dh.getDocument().execCommand('insertImage', false, dummyNodeId);
|
||||
var dummyImageNodePattern = new RegExp('<[^<]*' + dummyNodeId + '[^>]*>');
|
||||
var parent = this.getFieldObject().getRange().getContainerElement();
|
||||
if (parent.nodeType == goog.dom.NodeType.TEXT) {
|
||||
// Opera sometimes returns a text node here.
|
||||
// TODO(user): perhaps we should modify getParentContainer?
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
// We have to search up the DOM because in some cases, notably when
|
||||
// selecting li's within a list, execCommand('insertImage') actually splits
|
||||
// tags in such a way that parent that used to contain the selection does
|
||||
// not contain inserted image.
|
||||
while (!dummyImageNodePattern.test(parent.innerHTML)) {
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
// Like the IE case above, sometimes the selection does not include the
|
||||
// outermost tags. For Gecko, we have already expanded the range so that
|
||||
// it does, so we can just replace the dummy image with the final html.
|
||||
// For WebKit, we use the same approach as we do with IE - we
|
||||
// inject a dummy span where we will eventually place the contents, and
|
||||
// remove parentNodes of the span while they are empty.
|
||||
|
||||
if (goog.userAgent.GECKO) {
|
||||
goog.editor.node.replaceInnerHtml(parent,
|
||||
parent.innerHTML.replace(dummyImageNodePattern, html));
|
||||
} else {
|
||||
goog.editor.node.replaceInnerHtml(parent,
|
||||
parent.innerHTML.replace(dummyImageNodePattern, dummySpanText));
|
||||
var dummySpan = dh.getElement(dummyNodeId);
|
||||
parent = dummySpan;
|
||||
while ((parent = dummySpan.parentNode) &&
|
||||
goog.editor.node.isEmpty(parent) &&
|
||||
!goog.editor.node.isEditableContainer(parent)) {
|
||||
var tag = parent.nodeName;
|
||||
// We can't remove these table tags as it will invalidate the table dom.
|
||||
if (tag == goog.dom.TagName.TD ||
|
||||
tag == goog.dom.TagName.TR ||
|
||||
tag == goog.dom.TagName.TH) {
|
||||
break;
|
||||
}
|
||||
|
||||
// We can't just remove parent since dummySpan is inside it, and we need
|
||||
// to keep dummy span around for the replacement. So we move the
|
||||
// dummySpan up as we go.
|
||||
goog.dom.insertSiblingAfter(dummySpan, parent);
|
||||
goog.dom.removeNode(parent);
|
||||
}
|
||||
goog.editor.node.replaceInnerHtml(parent,
|
||||
parent.innerHTML.replace(new RegExp(dummySpanText, 'i'), html));
|
||||
}
|
||||
}
|
||||
|
||||
var startSpan = dh.getElement(startSpanId);
|
||||
var endSpan = dh.getElement(endSpanId);
|
||||
goog.dom.Range.createFromNodes(startSpan, 0, endSpan,
|
||||
endSpan.childNodes.length).select();
|
||||
goog.dom.removeNode(startSpan);
|
||||
goog.dom.removeNode(endSpan);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the html inside the selection to send off for further processing.
|
||||
*
|
||||
* TODO(user): Make this general so that it can be moved into
|
||||
* goog.editor.range. The main reason it can't be moved is becuase we need to
|
||||
* get the range before we do the execCommand and continue to operate on that
|
||||
* same range (reasons are documented above).
|
||||
*
|
||||
* @param {goog.dom.AbstractRange} range The selection.
|
||||
* @return {string} The html string to format.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.getHtmlText_ = function(range) {
|
||||
var div = this.getFieldDomHelper().createDom('div');
|
||||
var textRange = range.getBrowserRangeObject();
|
||||
|
||||
if (goog.editor.BrowserFeature.HAS_W3C_RANGES) {
|
||||
// Get the text to convert.
|
||||
div.appendChild(textRange.cloneContents());
|
||||
} else if (goog.editor.BrowserFeature.HAS_IE_RANGES) {
|
||||
// Trim the whitespace on the ends of the range, so that it the container
|
||||
// will be the container of only the text content that we are changing.
|
||||
// This gets around issues in IE where the spaces are included in the
|
||||
// selection, but ignored sometimes by execCommand, and left orphaned.
|
||||
var rngText = range.getText();
|
||||
|
||||
// BRs get reported as \r\n, but only count as one character for moves.
|
||||
// Adjust the string so our move counter is correct.
|
||||
rngText = rngText.replace(/\r\n/g, '\r');
|
||||
|
||||
var rngTextLength = rngText.length;
|
||||
var left = rngTextLength - goog.string.trimLeft(rngText).length;
|
||||
var right = rngTextLength - goog.string.trimRight(rngText).length;
|
||||
|
||||
textRange.moveStart('character', left);
|
||||
textRange.moveEnd('character', -right);
|
||||
|
||||
var htmlText = textRange.htmlText;
|
||||
// Check if in pretag and fix up formatting so that new lines are preserved.
|
||||
if (textRange.queryCommandValue('formatBlock') == 'Formatted') {
|
||||
htmlText = goog.string.newLineToBr(textRange.htmlText);
|
||||
}
|
||||
div.innerHTML = htmlText;
|
||||
}
|
||||
|
||||
// Get the innerHTML of the node instead of just returning the text above
|
||||
// so that its properly html escaped.
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Move the range so that it doesn't include any partially selected tables.
|
||||
* @param {goog.dom.AbstractRange} range The range to adjust.
|
||||
* @param {Node} startInTable Table node that the range starts in.
|
||||
* @param {Node} endInTable Table node that the range ends in.
|
||||
* @return {goog.dom.SavedCaretRange} Range to use to restore the
|
||||
* selection after we run our custom remove formatting.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.adjustRangeForTables_ =
|
||||
function(range, startInTable, endInTable) {
|
||||
// Create placeholders for the current selection so we can restore it
|
||||
// later.
|
||||
var savedCaretRange = goog.editor.range.saveUsingNormalizedCarets(range);
|
||||
|
||||
var startNode = range.getStartNode();
|
||||
var startOffset = range.getStartOffset();
|
||||
var endNode = range.getEndNode();
|
||||
var endOffset = range.getEndOffset();
|
||||
var dh = this.getFieldDomHelper();
|
||||
|
||||
// Move start after the table.
|
||||
if (startInTable) {
|
||||
var textNode = dh.createTextNode('');
|
||||
goog.dom.insertSiblingAfter(textNode, startInTable);
|
||||
startNode = textNode;
|
||||
startOffset = 0;
|
||||
}
|
||||
// Move end before the table.
|
||||
if (endInTable) {
|
||||
var textNode = dh.createTextNode('');
|
||||
goog.dom.insertSiblingBefore(textNode, endInTable);
|
||||
endNode = textNode;
|
||||
endOffset = 0;
|
||||
}
|
||||
|
||||
goog.dom.Range.createFromNodes(startNode, startOffset,
|
||||
endNode, endOffset).select();
|
||||
|
||||
return savedCaretRange;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove a caret from the dom and hide it in a safe place, so it can
|
||||
* be restored later via restoreCaretsFromCave.
|
||||
* @param {goog.dom.SavedCaretRange} caretRange The caret range to
|
||||
* get the carets from.
|
||||
* @param {boolean} isStart Whether this is the start or end caret.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.putCaretInCave_ = function(
|
||||
caretRange, isStart) {
|
||||
var cavedCaret = goog.dom.removeNode(caretRange.getCaret(isStart));
|
||||
if (isStart) {
|
||||
this.startCaretInCave_ = cavedCaret;
|
||||
} else {
|
||||
this.endCaretInCave_ = cavedCaret;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Restore carets that were hidden away by adding them back into the dom.
|
||||
* Note: this does not restore to the original dom location, as that
|
||||
* will likely have been modified with remove formatting. The only
|
||||
* guarentees here are that start will still be before end, and that
|
||||
* they will be in the editable region. This should only be used when
|
||||
* you don't actually intend to USE the caret again.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.restoreCaretsFromCave_ =
|
||||
function() {
|
||||
// To keep start before end, we put the end caret at the bottom of the field
|
||||
// and the start caret at the start of the field.
|
||||
var field = this.getFieldObject().getElement();
|
||||
if (this.startCaretInCave_) {
|
||||
field.insertBefore(this.startCaretInCave_, field.firstChild);
|
||||
this.startCaretInCave_ = null;
|
||||
}
|
||||
if (this.endCaretInCave_) {
|
||||
field.appendChild(this.endCaretInCave_);
|
||||
this.endCaretInCave_ = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the html inside the current selection, passes it through the given
|
||||
* conversion function, and puts it back into the selection.
|
||||
*
|
||||
* @param {function(string): string} convertFunc A conversion function that
|
||||
* transforms an html string to new html string.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.convertSelectedHtmlText_ =
|
||||
function(convertFunc) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
|
||||
// For multiple ranges, it is really hard to do our custom remove formatting
|
||||
// without invalidating other ranges. So instead of always losing the
|
||||
// content, this solution at least lets the browser do its own remove
|
||||
// formatting which works correctly most of the time.
|
||||
if (range.getTextRangeCount() > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (goog.userAgent.GECKO) {
|
||||
// Determine if we need to handle tables, since they are special cases.
|
||||
// If the selection is entirely within a table, there is no extra
|
||||
// formatting removal we can do. If a table is fully selected, we will
|
||||
// just blow it away. If a table is only partially selected, we can
|
||||
// perform custom remove formatting only on the non table parts, since we
|
||||
// we can't just remove the parts and paste back into it (eg. we can't
|
||||
// inject html where a TR used to be).
|
||||
// If the selection contains the table and more, this is automatically
|
||||
// handled, but if just the table is selected, it can be tricky to figure
|
||||
// this case out, because of the numerous ways selections can be formed -
|
||||
// ex. if a table has a single tr with a single td with a single text node
|
||||
// in it, and the selection is (textNode: 0), (textNode: nextNode.length)
|
||||
// then the entire table is selected, even though the start and end aren't
|
||||
// the table itself. We are truly inside a table if the expanded endpoints
|
||||
// are still inside the table.
|
||||
|
||||
// Expand the selection to include any outermost tags that weren't included
|
||||
// in the selection, but have the same visible selection. Stop expanding
|
||||
// if we reach the top level field.
|
||||
var expandedRange = goog.editor.range.expand(range,
|
||||
this.getFieldObject().getElement());
|
||||
|
||||
var startInTable = this.getTableAncestor_(expandedRange.getStartNode());
|
||||
var endInTable = this.getTableAncestor_(expandedRange.getEndNode());
|
||||
|
||||
if (startInTable || endInTable) {
|
||||
if (startInTable == endInTable) {
|
||||
// We are fully contained in the same table, there is no extra
|
||||
// remove formatting that we can do, just return and run browser
|
||||
// formatting only.
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust the range to not contain any partially selected tables, since
|
||||
// we don't want to run our custom remove formatting on them.
|
||||
var savedCaretRange = this.adjustRangeForTables_(range,
|
||||
startInTable, endInTable);
|
||||
|
||||
// Hack alert!!
|
||||
// If start is not in a table, then the saved caret will get sent out
|
||||
// for uber remove formatting, and it will get blown away. This is
|
||||
// fine, except that we need to be able to re-create a range from the
|
||||
// savedCaretRange later on. So, we just remove it from the dom, and
|
||||
// put it back later so we can create a range later (not exactly in the
|
||||
// same spot, but don't worry we don't actually try to use it later)
|
||||
// and then it will be removed when we dispose the range.
|
||||
if (!startInTable) {
|
||||
this.putCaretInCave_(savedCaretRange, true);
|
||||
}
|
||||
if (!endInTable) {
|
||||
this.putCaretInCave_(savedCaretRange, false);
|
||||
}
|
||||
|
||||
// Re-fetch the range, and re-expand it, since we just modified it.
|
||||
range = this.getFieldObject().getRange();
|
||||
expandedRange = goog.editor.range.expand(range,
|
||||
this.getFieldObject().getElement());
|
||||
}
|
||||
|
||||
expandedRange.select();
|
||||
range = expandedRange;
|
||||
}
|
||||
|
||||
// Convert the selected text to the format-less version, paste back into
|
||||
// the selection.
|
||||
var text = this.getHtmlText_(range);
|
||||
this.pasteHtml_(convertFunc(text));
|
||||
|
||||
if (goog.userAgent.GECKO && savedCaretRange) {
|
||||
// If we moved the selection, move it back so the user can't tell we did
|
||||
// anything crazy and so the browser removeFormat that we call next
|
||||
// will operate on the entire originally selected range.
|
||||
range = this.getFieldObject().getRange();
|
||||
this.restoreCaretsFromCave_();
|
||||
var realSavedCaretRange = savedCaretRange.toAbstractRange();
|
||||
var startRange = startInTable ? realSavedCaretRange : range;
|
||||
var endRange = endInTable ? realSavedCaretRange : range;
|
||||
var restoredRange =
|
||||
goog.editor.plugins.RemoveFormatting.createRangeDelimitedByRanges_(
|
||||
startRange, endRange);
|
||||
restoredRange.select();
|
||||
savedCaretRange.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Does a best-effort attempt at clobbering all formatting that the
|
||||
* browser's execCommand couldn't clobber without being totally inefficient.
|
||||
* Attempts to convert visual line breaks to BRs. Leaves anchors that contain an
|
||||
* href and images.
|
||||
* Adapted from Gmail's MessageUtil's htmlToPlainText. http://go/messageutil.js
|
||||
* @param {string} html The original html of the message.
|
||||
* @return {string} The unformatted html, which is just text, br's, anchors and
|
||||
* images.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.removeFormattingWorker_ =
|
||||
function(html) {
|
||||
var el = goog.dom.createElement('div');
|
||||
el.innerHTML = html;
|
||||
|
||||
// Put everything into a string buffer to avoid lots of expensive string
|
||||
// concatenation along the way.
|
||||
var sb = [];
|
||||
var stack = [el.childNodes, 0];
|
||||
|
||||
// Keep separate stacks for places where we need to keep track of
|
||||
// how deeply embedded we are. These are analogous to the general stack.
|
||||
var preTagStack = [];
|
||||
var preTagLevel = 0; // Length of the prestack.
|
||||
var tableStack = [];
|
||||
var tableLevel = 0;
|
||||
|
||||
// sp = stack pointer, pointing to the stack array.
|
||||
// decrement by 2 since the stack alternates node lists and
|
||||
// processed node counts
|
||||
for (var sp = 0; sp >= 0; sp -= 2) {
|
||||
// Check if we should pop the table level.
|
||||
var changedLevel = false;
|
||||
while (tableLevel > 0 && sp <= tableStack[tableLevel - 1]) {
|
||||
tableLevel--;
|
||||
changedLevel = true;
|
||||
}
|
||||
if (changedLevel) {
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
}
|
||||
|
||||
|
||||
// Check if we should pop the <pre>/<xmp> level.
|
||||
changedLevel = false;
|
||||
while (preTagLevel > 0 && sp <= preTagStack[preTagLevel - 1]) {
|
||||
preTagLevel--;
|
||||
changedLevel = true;
|
||||
}
|
||||
if (changedLevel) {
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
}
|
||||
|
||||
// The list of of nodes to process at the current stack level.
|
||||
var nodeList = stack[sp];
|
||||
// The number of nodes processed so far, stored in the stack immediately
|
||||
// following the node list for that stack level.
|
||||
var numNodesProcessed = stack[sp + 1];
|
||||
|
||||
while (numNodesProcessed < nodeList.length) {
|
||||
var node = nodeList[numNodesProcessed++];
|
||||
var nodeName = node.nodeName;
|
||||
|
||||
var formatted = this.getValueForNode(node);
|
||||
if (goog.isDefAndNotNull(formatted)) {
|
||||
sb.push(formatted);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO(user): Handle case 'EMBED' and case 'OBJECT'.
|
||||
switch (nodeName) {
|
||||
case '#text':
|
||||
// Note that IE does not preserve whitespace in the dom
|
||||
// values, even in a pre tag, so this is useless for IE.
|
||||
var nodeValue = preTagLevel > 0 ?
|
||||
node.nodeValue :
|
||||
goog.string.stripNewlines(node.nodeValue);
|
||||
nodeValue = goog.string.htmlEscape(nodeValue);
|
||||
sb.push(nodeValue);
|
||||
continue;
|
||||
|
||||
case goog.dom.TagName.P:
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
break; // break (not continue) so that child nodes are processed.
|
||||
|
||||
case goog.dom.TagName.BR:
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
continue;
|
||||
|
||||
case goog.dom.TagName.TABLE:
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
tableStack[tableLevel++] = sp;
|
||||
break;
|
||||
|
||||
case goog.dom.TagName.PRE:
|
||||
case 'XMP':
|
||||
// This doesn't fully handle xmp, since
|
||||
// it doesn't actually ignore tags within the xmp tag.
|
||||
preTagStack[preTagLevel++] = sp;
|
||||
break;
|
||||
|
||||
case goog.dom.TagName.STYLE:
|
||||
case goog.dom.TagName.SCRIPT:
|
||||
case goog.dom.TagName.SELECT:
|
||||
continue;
|
||||
|
||||
case goog.dom.TagName.A:
|
||||
if (node.href && node.href != '') {
|
||||
sb.push("<a href='");
|
||||
sb.push(node.href);
|
||||
sb.push("'>");
|
||||
sb.push(this.removeFormattingWorker_(node.innerHTML));
|
||||
sb.push('</a>');
|
||||
continue; // Children taken care of.
|
||||
} else {
|
||||
break; // Take care of the children.
|
||||
}
|
||||
|
||||
case goog.dom.TagName.IMG:
|
||||
sb.push("<img src='");
|
||||
sb.push(node.src);
|
||||
sb.push("'");
|
||||
// border=0 is a common way to not show a blue border around an image
|
||||
// that is wrapped by a link. If we remove that, the blue border will
|
||||
// show up, which to the user looks like adding format, not removing.
|
||||
if (node.border == '0') {
|
||||
sb.push(" border='0'");
|
||||
}
|
||||
sb.push('>');
|
||||
continue;
|
||||
|
||||
case goog.dom.TagName.TD:
|
||||
// Don't add a space for the first TD, we only want spaces to
|
||||
// separate td's.
|
||||
if (node.previousSibling) {
|
||||
sb.push(' ');
|
||||
}
|
||||
break;
|
||||
|
||||
case goog.dom.TagName.TR:
|
||||
// Don't add a newline for the first TR.
|
||||
if (node.previousSibling) {
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
}
|
||||
break;
|
||||
|
||||
case goog.dom.TagName.DIV:
|
||||
var parent = node.parentNode;
|
||||
if (parent.firstChild == node &&
|
||||
goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(
|
||||
parent.tagName)) {
|
||||
// If a DIV is the first child of another element that itself is a
|
||||
// block element, the DIV does not add a new line.
|
||||
break;
|
||||
}
|
||||
// Otherwise, the DIV does add a new line. Fall through.
|
||||
|
||||
default:
|
||||
if (goog.editor.plugins.RemoveFormatting.BLOCK_RE_.test(nodeName)) {
|
||||
goog.editor.plugins.RemoveFormatting.appendNewline_(sb);
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse down the node.
|
||||
var children = node.childNodes;
|
||||
if (children.length > 0) {
|
||||
// Push the current state on the stack.
|
||||
stack[sp++] = nodeList;
|
||||
stack[sp++] = numNodesProcessed;
|
||||
|
||||
// Iterate through the children nodes.
|
||||
nodeList = children;
|
||||
numNodesProcessed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace with white space.
|
||||
return goog.string.normalizeSpaces(sb.join(''));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle per node special processing if neccessary. If this function returns
|
||||
* null then standard cleanup is applied. Otherwise this node and all children
|
||||
* are assumed to be cleaned.
|
||||
* NOTE(user): If an alternate RemoveFormatting processor is provided
|
||||
* (setRemoveFormattingFunc()), this will no longer work.
|
||||
* @param {Element} node The node to clean.
|
||||
* @return {?string} The HTML strig representation of the cleaned data.
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.getValueForNode = function(
|
||||
node) {
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets a function to be used for remove formatting.
|
||||
* @param {function(string): string} removeFormattingFunc - A function that
|
||||
* takes a string of html and returns a string of html that does any other
|
||||
* formatting changes desired. Use this only if trogedit's behavior doesn't
|
||||
* meet your needs.
|
||||
*/
|
||||
goog.editor.plugins.RemoveFormatting.prototype.setRemoveFormattingFunc =
|
||||
function(removeFormattingFunc) {
|
||||
this.optRemoveFormattingFunc_ = removeFormattingFunc;
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Editor plugin to handle tab keys not in lists to add 4 spaces.
|
||||
*
|
||||
* @author robbyw@google.com (Robby Walker)
|
||||
* @author ajp@google.com (Andy Perelson)
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.SpacesTabHandler');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.plugins.AbstractTabHandler');
|
||||
goog.require('goog.editor.range');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to handle tab keys when not in lists to add 4 spaces.
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.AbstractTabHandler}
|
||||
*/
|
||||
goog.editor.plugins.SpacesTabHandler = function() {
|
||||
goog.editor.plugins.AbstractTabHandler.call(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.SpacesTabHandler,
|
||||
goog.editor.plugins.AbstractTabHandler);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.SpacesTabHandler.prototype.getTrogClassId = function() {
|
||||
return 'SpacesTabHandler';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.SpacesTabHandler.prototype.handleTabKey = function(e) {
|
||||
var dh = this.getFieldDomHelper();
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (!goog.editor.range.intersectsTag(range, goog.dom.TagName.LI)) {
|
||||
// In the shift + tab case we don't want to insert spaces, but we don't
|
||||
// want focus to move either so skip the spacing logic and just prevent
|
||||
// default.
|
||||
if (!e.shiftKey) {
|
||||
// Not in a list but we want to insert 4 spaces.
|
||||
|
||||
// Stop change events while we make multiple field changes.
|
||||
this.getFieldObject().stopChangeEvents(true, true);
|
||||
|
||||
// Inserting nodes below completely messes up the selection, doing the
|
||||
// deletion here before it's messed up. Only delete if text is selected,
|
||||
// otherwise we would remove the character to the right of the cursor.
|
||||
if (!range.isCollapsed()) {
|
||||
dh.getDocument().execCommand('delete', false, null);
|
||||
// Safari 3 has some DOM exceptions if we don't reget the range here,
|
||||
// doing it all the time just to be safe.
|
||||
range = this.getFieldObject().getRange();
|
||||
}
|
||||
|
||||
// Emulate tab by removing selection and inserting 4 spaces
|
||||
// Two breaking spaces in a row can be collapsed by the browser into one
|
||||
// space. Inserting the string below because it is guaranteed to never
|
||||
// collapse to less than four spaces, regardless of what is adjacent to
|
||||
// the inserted spaces. This might make line wrapping slightly
|
||||
// sub-optimal around a grouping of non-breaking spaces.
|
||||
var elem = dh.createDom('span', null, '\u00a0\u00a0 \u00a0');
|
||||
elem = range.insertNode(elem, false);
|
||||
|
||||
this.getFieldObject().dispatchChange();
|
||||
goog.editor.range.placeCursorNextTo(elem, false);
|
||||
this.getFieldObject().dispatchSelectionChangeEvent();
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Plugin that enables table editing.
|
||||
*
|
||||
* @see ../../demos/editor/tableeditor.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.TableEditor');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.Plugin');
|
||||
goog.require('goog.editor.Table');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.object');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin that adds support for table creation and editing commands.
|
||||
* @constructor
|
||||
* @extends {goog.editor.Plugin}
|
||||
*/
|
||||
goog.editor.plugins.TableEditor = function() {
|
||||
goog.base(this);
|
||||
|
||||
/**
|
||||
* The array of functions that decide whether a table element could be
|
||||
* editable by the user or not.
|
||||
* @type {Array.<function(Element):boolean>}
|
||||
* @private
|
||||
*/
|
||||
this.isTableEditableFunctions_ = [];
|
||||
|
||||
/**
|
||||
* The pre-bound function that decides whether a table element could be
|
||||
* editable by the user or not overall.
|
||||
* @type {function(Node):boolean}
|
||||
* @private
|
||||
*/
|
||||
this.isUserEditableTableBound_ = goog.bind(this.isUserEditableTable_, this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.TableEditor, goog.editor.Plugin);
|
||||
|
||||
|
||||
/** @override */
|
||||
// TODO(user): remove this once there's a sensible default
|
||||
// implementation in the base Plugin.
|
||||
goog.editor.plugins.TableEditor.prototype.getTrogClassId = function() {
|
||||
return String(goog.getUid(this.constructor));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Commands supported by goog.editor.plugins.TableEditor.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.COMMAND = {
|
||||
TABLE: '+table',
|
||||
INSERT_ROW_AFTER: '+insertRowAfter',
|
||||
INSERT_ROW_BEFORE: '+insertRowBefore',
|
||||
INSERT_COLUMN_AFTER: '+insertColumnAfter',
|
||||
INSERT_COLUMN_BEFORE: '+insertColumnBefore',
|
||||
REMOVE_ROWS: '+removeRows',
|
||||
REMOVE_COLUMNS: '+removeColumns',
|
||||
SPLIT_CELL: '+splitCell',
|
||||
MERGE_CELLS: '+mergeCells',
|
||||
REMOVE_TABLE: '+removeTable'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inverse map of execCommand strings to
|
||||
* {@link goog.editor.plugins.TableEditor.COMMAND} constants. Used to
|
||||
* determine whether a string corresponds to a command this plugin handles
|
||||
* in O(1) time.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_ =
|
||||
goog.object.transpose(goog.editor.plugins.TableEditor.COMMAND);
|
||||
|
||||
|
||||
/**
|
||||
* Whether the string corresponds to a command this plugin handles.
|
||||
* @param {string} command Command string to check.
|
||||
* @return {boolean} Whether the string corresponds to a command
|
||||
* this plugin handles.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.prototype.isSupportedCommand =
|
||||
function(command) {
|
||||
return command in goog.editor.plugins.TableEditor.SUPPORTED_COMMANDS_;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TableEditor.prototype.enable = function(fieldObject) {
|
||||
goog.base(this, 'enable', fieldObject);
|
||||
|
||||
// enableObjectResizing is supported only for Gecko.
|
||||
// You can refer to http://qooxdoo.org/contrib/project/htmlarea/html_editing
|
||||
// for a compatibility chart.
|
||||
if (goog.userAgent.GECKO) {
|
||||
var doc = this.getFieldDomHelper().getDocument();
|
||||
doc.execCommand('enableObjectResizing', false, 'true');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the currently selected table.
|
||||
* @return {Element?} The table in which the current selection is
|
||||
* contained, or null if there isn't such a table.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.prototype.getCurrentTable_ = function() {
|
||||
var selectedElement = this.getFieldObject().getRange().getContainer();
|
||||
return this.getAncestorTable_(selectedElement);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Finds the first user-editable table element in the input node's ancestors.
|
||||
* @param {Node?} node The node to start with.
|
||||
* @return {Element?} The table element that is closest ancestor of the node.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.prototype.getAncestorTable_ = function(node) {
|
||||
var ancestor = goog.dom.getAncestor(node, this.isUserEditableTableBound_,
|
||||
true);
|
||||
if (goog.editor.node.isEditable(ancestor)) {
|
||||
return /** @type {Element?} */(ancestor);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current value of a given command. Currently this plugin
|
||||
* only returns a value for goog.editor.plugins.TableEditor.COMMAND.TABLE.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.prototype.queryCommandValue =
|
||||
function(command) {
|
||||
if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) {
|
||||
return !!this.getCurrentTable_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TableEditor.prototype.execCommandInternal = function(
|
||||
command, opt_arg) {
|
||||
var result = null;
|
||||
// TD/TH in which to place the cursor, if the command destroys the current
|
||||
// cursor position.
|
||||
var cursorCell = null;
|
||||
var range = this.getFieldObject().getRange();
|
||||
if (command == goog.editor.plugins.TableEditor.COMMAND.TABLE) {
|
||||
// Don't create a table if the cursor isn't in an editable region.
|
||||
if (!goog.editor.range.isEditable(range)) {
|
||||
return null;
|
||||
}
|
||||
// Create the table.
|
||||
var tableProps = opt_arg || {width: 4, height: 2};
|
||||
var doc = this.getFieldDomHelper().getDocument();
|
||||
var table = goog.editor.Table.createDomTable(
|
||||
doc, tableProps.width, tableProps.height);
|
||||
range.replaceContentsWithNode(table);
|
||||
// In IE, replaceContentsWithNode uses pasteHTML, so we lose our reference
|
||||
// to the inserted table.
|
||||
// TODO(user): use the reference to the table element returned from
|
||||
// replaceContentsWithNode.
|
||||
if (!goog.userAgent.IE) {
|
||||
cursorCell = table.getElementsByTagName('td')[0];
|
||||
}
|
||||
} else {
|
||||
var cellSelection = new goog.editor.plugins.TableEditor.CellSelection_(
|
||||
range, goog.bind(this.getAncestorTable_, this));
|
||||
var table = cellSelection.getTable();
|
||||
if (!table) {
|
||||
return null;
|
||||
}
|
||||
switch (command) {
|
||||
case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_BEFORE:
|
||||
table.insertRow(cellSelection.getFirstRowIndex());
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.INSERT_ROW_AFTER:
|
||||
table.insertRow(cellSelection.getLastRowIndex() + 1);
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_BEFORE:
|
||||
table.insertColumn(cellSelection.getFirstColumnIndex());
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.INSERT_COLUMN_AFTER:
|
||||
table.insertColumn(cellSelection.getLastColumnIndex() + 1);
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.REMOVE_ROWS:
|
||||
var startRow = cellSelection.getFirstRowIndex();
|
||||
var endRow = cellSelection.getLastRowIndex();
|
||||
if (startRow == 0 && endRow == (table.rows.length - 1)) {
|
||||
// Instead of deleting all rows, delete the entire table.
|
||||
return this.execCommandInternal(
|
||||
goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE);
|
||||
}
|
||||
var startColumn = cellSelection.getFirstColumnIndex();
|
||||
var rowCount = (endRow - startRow) + 1;
|
||||
for (var i = 0; i < rowCount; i++) {
|
||||
table.removeRow(startRow);
|
||||
}
|
||||
if (table.rows.length > 0) {
|
||||
// Place cursor in the previous/first row.
|
||||
var closestRow = Math.min(startRow, table.rows.length - 1);
|
||||
cursorCell = table.rows[closestRow].columns[startColumn].element;
|
||||
}
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.REMOVE_COLUMNS:
|
||||
var startCol = cellSelection.getFirstColumnIndex();
|
||||
var endCol = cellSelection.getLastColumnIndex();
|
||||
if (startCol == 0 && endCol == (table.rows[0].columns.length - 1)) {
|
||||
// Instead of deleting all columns, delete the entire table.
|
||||
return this.execCommandInternal(
|
||||
goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE);
|
||||
}
|
||||
var startRow = cellSelection.getFirstRowIndex();
|
||||
var removeCount = (endCol - startCol) + 1;
|
||||
for (var i = 0; i < removeCount; i++) {
|
||||
table.removeColumn(startCol);
|
||||
}
|
||||
var currentRow = table.rows[startRow];
|
||||
if (currentRow) {
|
||||
// Place cursor in the previous/first column.
|
||||
var closestCol = Math.min(startCol, currentRow.columns.length - 1);
|
||||
cursorCell = currentRow.columns[closestCol].element;
|
||||
}
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.MERGE_CELLS:
|
||||
if (cellSelection.isRectangle()) {
|
||||
table.mergeCells(cellSelection.getFirstRowIndex(),
|
||||
cellSelection.getFirstColumnIndex(),
|
||||
cellSelection.getLastRowIndex(),
|
||||
cellSelection.getLastColumnIndex());
|
||||
}
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.SPLIT_CELL:
|
||||
if (cellSelection.containsSingleCell()) {
|
||||
table.splitCell(cellSelection.getFirstRowIndex(),
|
||||
cellSelection.getFirstColumnIndex());
|
||||
}
|
||||
break;
|
||||
case goog.editor.plugins.TableEditor.COMMAND.REMOVE_TABLE:
|
||||
table.element.parentNode.removeChild(table.element);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
if (cursorCell) {
|
||||
range = goog.dom.Range.createFromNodeContents(cursorCell);
|
||||
range.collapse(false);
|
||||
range.select();
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether the element is a table editable by the user.
|
||||
* @param {Node} element The element in question.
|
||||
* @return {boolean} Whether the element is a table editable by the user.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.prototype.isUserEditableTable_ =
|
||||
function(element) {
|
||||
// Default implementation.
|
||||
if (element.tagName != goog.dom.TagName.TABLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for extra user-editable filters.
|
||||
return goog.array.every(this.isTableEditableFunctions_, function(func) {
|
||||
return func(/** @type {Element} */ (element));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds a function to filter out non-user-editable tables.
|
||||
* @param {function(Element):boolean} func A function to decide whether the
|
||||
* table element could be editable by the user or not.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.prototype.addIsTableEditableFunction =
|
||||
function(func) {
|
||||
goog.array.insert(this.isTableEditableFunctions_, func);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Class representing the selected cell objects within a single table.
|
||||
* @param {goog.dom.AbstractRange} range Selected range from which to calculate
|
||||
* selected cells.
|
||||
* @param {function(Element):Element?} getParentTableFunction A function that
|
||||
* finds the user-editable table from a given element.
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_ =
|
||||
function(range, getParentTableFunction) {
|
||||
this.cells_ = [];
|
||||
|
||||
// Mozilla lets users select groups of cells, with each cell showing
|
||||
// up as a separate range in the selection. goog.dom.Range doesn't
|
||||
// currently support this.
|
||||
// TODO(user): support this case in range.js
|
||||
var selectionContainer = range.getContainerElement();
|
||||
var elementInSelection = function(node) {
|
||||
// TODO(user): revert to the more liberal containsNode(node, true),
|
||||
// which will match partially-selected cells. We're using
|
||||
// containsNode(node, false) at the moment because otherwise it's
|
||||
// broken in WebKit due to a closure range bug.
|
||||
return selectionContainer == node ||
|
||||
selectionContainer.parentNode == node ||
|
||||
range.containsNode(node, false);
|
||||
};
|
||||
|
||||
var parentTableElement = selectionContainer &&
|
||||
getParentTableFunction(selectionContainer);
|
||||
if (!parentTableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parentTable = new goog.editor.Table(parentTableElement);
|
||||
// It's probably not possible to select a table with no cells, but
|
||||
// do a sanity check anyway.
|
||||
if (!parentTable.rows.length || !parentTable.rows[0].columns.length) {
|
||||
return;
|
||||
}
|
||||
// Loop through cells to calculate dimensions for this CellSelection.
|
||||
for (var i = 0, row; row = parentTable.rows[i]; i++) {
|
||||
for (var j = 0, cell; cell = row.columns[j]; j++) {
|
||||
if (elementInSelection(cell.element)) {
|
||||
// Update dimensions based on cell.
|
||||
if (!this.cells_.length) {
|
||||
this.firstRowIndex_ = cell.startRow;
|
||||
this.lastRowIndex_ = cell.endRow;
|
||||
this.firstColIndex_ = cell.startCol;
|
||||
this.lastColIndex_ = cell.endCol;
|
||||
} else {
|
||||
this.firstRowIndex_ = Math.min(this.firstRowIndex_, cell.startRow);
|
||||
this.lastRowIndex_ = Math.max(this.lastRowIndex_, cell.endRow);
|
||||
this.firstColIndex_ = Math.min(this.firstColIndex_, cell.startCol);
|
||||
this.lastColIndex_ = Math.max(this.lastColIndex_, cell.endCol);
|
||||
}
|
||||
this.cells_.push(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.parentTable_ = parentTable;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the EditableTable object of which this selection's cells are a
|
||||
* subset.
|
||||
* @return {goog.editor.Table?} the table.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.getTable =
|
||||
function() {
|
||||
return this.parentTable_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the row index of the uppermost cell in this selection.
|
||||
* @return {number} The row index.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstRowIndex =
|
||||
function() {
|
||||
return this.firstRowIndex_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the row index of the lowermost cell in this selection.
|
||||
* @return {number} The row index.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastRowIndex =
|
||||
function() {
|
||||
return this.lastRowIndex_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the column index of the farthest left cell in this selection.
|
||||
* @return {number} The column index.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.getFirstColumnIndex =
|
||||
function() {
|
||||
return this.firstColIndex_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the column index of the farthest right cell in this selection.
|
||||
* @return {number} The column index.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.getLastColumnIndex =
|
||||
function() {
|
||||
return this.lastColIndex_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the cells in this selection.
|
||||
* @return {Array.<Element>} Cells in this selection.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.getCells = function() {
|
||||
return this.cells_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether or not the cells in this
|
||||
* selection form a rectangle.
|
||||
* @return {boolean} Whether the selection forms a rectangle.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.isRectangle =
|
||||
function() {
|
||||
// TODO(user): check for missing cells. Right now this returns
|
||||
// whether all cells in the selection are in the rectangle, but doesn't
|
||||
// verify that every expected cell is present.
|
||||
if (!this.cells_.length) {
|
||||
return false;
|
||||
}
|
||||
var firstCell = this.cells_[0];
|
||||
var lastCell = this.cells_[this.cells_.length - 1];
|
||||
return !(this.firstRowIndex_ < firstCell.startRow ||
|
||||
this.lastRowIndex_ > lastCell.endRow ||
|
||||
this.firstColIndex_ < firstCell.startCol ||
|
||||
this.lastColIndex_ > lastCell.endCol);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a boolean value indicating whether or not there is exactly
|
||||
* one cell in this selection. Note that this may not be the same as checking
|
||||
* whether getCells().length == 1; if there is a single cell with
|
||||
* rowSpan/colSpan set it will appear multiple times.
|
||||
* @return {boolean} Whether there is exatly one cell in this selection.
|
||||
*/
|
||||
goog.editor.plugins.TableEditor.CellSelection_.prototype.containsSingleCell =
|
||||
function() {
|
||||
var cellCount = this.cells_.length;
|
||||
return cellCount > 0 &&
|
||||
(this.cells_[0] == this.cells_[cellCount - 1]);
|
||||
};
|
||||
@@ -0,0 +1,742 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview TrogEdit plugin to handle enter keys by inserting the
|
||||
* specified block level tag.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.editor.plugins.TagOnEnterHandler');
|
||||
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.Range');
|
||||
goog.require('goog.dom.TagName');
|
||||
goog.require('goog.editor.Command');
|
||||
goog.require('goog.editor.node');
|
||||
goog.require('goog.editor.plugins.EnterHandler');
|
||||
goog.require('goog.editor.range');
|
||||
goog.require('goog.editor.style');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.string');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Plugin to handle enter keys. This subclass normalizes all browsers to use
|
||||
* the given block tag on enter.
|
||||
* @param {goog.dom.TagName} tag The type of tag to add on enter.
|
||||
* @constructor
|
||||
* @extends {goog.editor.plugins.EnterHandler}
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler = function(tag) {
|
||||
this.tag = tag;
|
||||
|
||||
goog.editor.plugins.EnterHandler.call(this);
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.TagOnEnterHandler,
|
||||
goog.editor.plugins.EnterHandler);
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.getTrogClassId = function() {
|
||||
return 'TagOnEnterHandler';
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.getNonCollapsingBlankHtml =
|
||||
function() {
|
||||
if (this.tag == goog.dom.TagName.P) {
|
||||
return '<p> </p>';
|
||||
} else if (this.tag == goog.dom.TagName.DIV) {
|
||||
return '<div><br></div>';
|
||||
}
|
||||
return '<br>';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* This plugin is active on uneditable fields so it can provide a value for
|
||||
* queryCommandValue calls asking for goog.editor.Command.BLOCKQUOTE.
|
||||
* @return {boolean} True.
|
||||
* @override
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.activeOnUneditableFields =
|
||||
goog.functions.TRUE;
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.isSupportedCommand = function(
|
||||
command) {
|
||||
return command == goog.editor.Command.DEFAULT_TAG;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.queryCommandValue = function(
|
||||
command) {
|
||||
return command == goog.editor.Command.DEFAULT_TAG ? this.tag : null;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.handleBackspaceInternal =
|
||||
function(e, range) {
|
||||
goog.editor.plugins.TagOnEnterHandler.superClass_.handleBackspaceInternal.
|
||||
call(this, e, range);
|
||||
|
||||
if (goog.userAgent.GECKO) {
|
||||
this.markBrToNotBeRemoved_(range, true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.processParagraphTagsInternal =
|
||||
function(e, split) {
|
||||
if ((goog.userAgent.OPERA || goog.userAgent.IE) &&
|
||||
this.tag != goog.dom.TagName.P) {
|
||||
this.ensureBlockIeOpera(this.tag);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.handleDeleteGecko = function(
|
||||
e) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
var container = goog.editor.style.getContainer(
|
||||
range && range.getContainerElement());
|
||||
if (this.getFieldObject().getElement().lastChild == container &&
|
||||
goog.editor.plugins.EnterHandler.isBrElem(container)) {
|
||||
// Don't delete if it's the last node in the field and just has a BR.
|
||||
e.preventDefault();
|
||||
// TODO(user): I think we probably don't need to stopPropagation here
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
// Go ahead with deletion.
|
||||
// Prevent an existing BR immediately following the selection being deleted
|
||||
// from being removed in the keyup stage (as opposed to a BR added by FF
|
||||
// after deletion, which we do remove).
|
||||
this.markBrToNotBeRemoved_(range, false);
|
||||
// Manually delete the selection if it's at a BR.
|
||||
this.deleteBrGecko(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.handleKeyUpInternal = function(
|
||||
e) {
|
||||
if (goog.userAgent.GECKO) {
|
||||
if (e.keyCode == goog.events.KeyCodes.DELETE) {
|
||||
this.removeBrIfNecessary_(false);
|
||||
} else if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
|
||||
this.removeBrIfNecessary_(true);
|
||||
}
|
||||
} else if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
|
||||
e.keyCode == goog.events.KeyCodes.ENTER) {
|
||||
this.ensureBlockIeOpera(this.tag, true);
|
||||
}
|
||||
// Safari uses DIVs by default.
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* String that matches a single BR tag or NBSP surrounded by non-breaking
|
||||
* whitespace
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.BrOrNbspSurroundedWithWhiteSpace_ =
|
||||
'[\t\n\r ]*(<br[^>]*\/?>| )[\t\n\r ]*';
|
||||
|
||||
|
||||
/**
|
||||
* String that matches a single BR tag or NBSP surrounded by non-breaking
|
||||
* whitespace
|
||||
* @type {RegExp}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.emptyLiRegExp_ = new RegExp('^' +
|
||||
goog.editor.plugins.TagOnEnterHandler.BrOrNbspSurroundedWithWhiteSpace_ +
|
||||
'$');
|
||||
|
||||
|
||||
/**
|
||||
* Ensures the current node is wrapped in the tag.
|
||||
* @param {Node} node The node to ensure gets wrapped.
|
||||
* @param {Element} container Element containing the selection.
|
||||
* @return {Element} Element containing the selection, after the wrapping.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.ensureNodeIsWrappedW3c_ =
|
||||
function(node, container) {
|
||||
if (container == this.getFieldObject().getElement()) {
|
||||
// If the first block-level ancestor of cursor is the field,
|
||||
// don't split the tree. Find all the text from the cursor
|
||||
// to both block-level elements surrounding it (if they exist)
|
||||
// and split the text into two elements.
|
||||
// This is the IE contentEditable behavior.
|
||||
|
||||
// The easy way to do this is to wrap all the text in an element
|
||||
// and then split the element as if the user had hit enter
|
||||
// in the paragraph
|
||||
|
||||
// However, simply wrapping the text into an element creates problems
|
||||
// if the text was already wrapped using some other element such as an
|
||||
// anchor. For example, wrapping the text of
|
||||
// <a href="">Text</a>
|
||||
// would produce
|
||||
// <a href=""><p>Text</p></a>
|
||||
// which is not what we want. What we really want is
|
||||
// <p><a href="">Text</a></p>
|
||||
// So we need to search for an ancestor of position.node to be wrapped.
|
||||
// We do this by iterating up the hierarchy of postiion.node until we've
|
||||
// reached the node that's just under the container.
|
||||
var isChildOfFn = function(child) {
|
||||
return container == child.parentNode; };
|
||||
var nodeToWrap = goog.dom.getAncestor(node, isChildOfFn, true);
|
||||
container = goog.editor.plugins.TagOnEnterHandler.wrapInContainerW3c_(
|
||||
this.tag, {node: nodeToWrap, offset: 0}, container);
|
||||
}
|
||||
return container;
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.handleEnterWebkitInternal =
|
||||
function(e) {
|
||||
if (this.tag == goog.dom.TagName.DIV) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
var container =
|
||||
goog.editor.style.getContainer(range.getContainerElement());
|
||||
|
||||
var position = goog.editor.range.getDeepEndPoint(range, true);
|
||||
container = this.ensureNodeIsWrappedW3c_(position.node, container);
|
||||
goog.dom.Range.createCaret(position.node, position.offset).select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.
|
||||
handleEnterAtCursorGeckoInternal = function(e, wasCollapsed, range) {
|
||||
// We use this because there are a few cases where FF default
|
||||
// implementation doesn't follow IE's:
|
||||
// -Inserts BRs into empty elements instead of NBSP which has nasty
|
||||
// side effects w/ making/deleting selections
|
||||
// -Hitting enter when your cursor is in the field itself. IE will
|
||||
// create two elements. FF just inserts a BR.
|
||||
// -Hitting enter inside an empty list-item doesn't create a block
|
||||
// tag. It just splits the list and puts your cursor in the middle.
|
||||
var li = null;
|
||||
if (wasCollapsed) {
|
||||
// Only break out of lists for collapsed selections.
|
||||
li = goog.dom.getAncestorByTagNameAndClass(
|
||||
range && range.getContainerElement(), goog.dom.TagName.LI);
|
||||
}
|
||||
var isEmptyLi = (li &&
|
||||
li.innerHTML.match(
|
||||
goog.editor.plugins.TagOnEnterHandler.emptyLiRegExp_));
|
||||
var elementAfterCursor = isEmptyLi ?
|
||||
this.breakOutOfEmptyListItemGecko_(li) :
|
||||
this.handleRegularEnterGecko_();
|
||||
|
||||
// Move the cursor in front of "nodeAfterCursor", and make sure it
|
||||
// is visible
|
||||
this.scrollCursorIntoViewGecko_(elementAfterCursor);
|
||||
|
||||
// Fix for http://b/1991234 :
|
||||
if (goog.editor.plugins.EnterHandler.isBrElem(elementAfterCursor)) {
|
||||
// The first element in the new line is a line with just a BR and maybe some
|
||||
// whitespace.
|
||||
// Calling normalize() is needed because there might be empty text nodes
|
||||
// before BR and empty text nodes cause the cursor position bug in Firefox.
|
||||
// See http://b/5220858
|
||||
elementAfterCursor.normalize();
|
||||
var br = elementAfterCursor.getElementsByTagName(goog.dom.TagName.BR)[0];
|
||||
if (br.previousSibling &&
|
||||
br.previousSibling.nodeType == goog.dom.NodeType.TEXT) {
|
||||
// If there is some whitespace before the BR, don't put the selection on
|
||||
// the BR, put it in the text node that's there, otherwise when you type
|
||||
// it will create adjacent text nodes.
|
||||
elementAfterCursor = br.previousSibling;
|
||||
}
|
||||
}
|
||||
|
||||
goog.editor.range.selectNodeStart(elementAfterCursor);
|
||||
|
||||
e.preventDefault();
|
||||
// TODO(user): I think we probably don't need to stopPropagation here
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If The cursor is in an empty LI then break out of the list like in IE
|
||||
* @param {Node} li LI to break out of.
|
||||
* @return {Element} Element to put the cursor after.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.breakOutOfEmptyListItemGecko_ =
|
||||
function(li) {
|
||||
// Do this as follows:
|
||||
// 1. <ul>...<li> </li>...</ul>
|
||||
// 2. <ul id='foo1'>...<li id='foo2'> </li>...</ul>
|
||||
// 3. <ul id='foo1'>...</ul><p id='foo3'> </p><ul id='foo2'>...</ul>
|
||||
// 4. <ul>...</ul><p> </p><ul>...</ul>
|
||||
//
|
||||
// There are a couple caveats to the above. If the UL is contained in
|
||||
// a list, then the new node inserted is an LI, not a P.
|
||||
// For an OL, it's all the same, except the tagname of course.
|
||||
// Finally, it's possible that with the LI at the beginning or the end
|
||||
// of the list that we'll end up with an empty list. So we special case
|
||||
// those cases.
|
||||
|
||||
var listNode = li.parentNode;
|
||||
var grandparent = listNode.parentNode;
|
||||
var inSubList = grandparent.tagName == goog.dom.TagName.OL ||
|
||||
grandparent.tagName == goog.dom.TagName.UL;
|
||||
|
||||
// TODO(robbyw): Should we apply the list or list item styles to the new node?
|
||||
var newNode = goog.dom.getDomHelper(li).createElement(
|
||||
inSubList ? goog.dom.TagName.LI : this.tag);
|
||||
|
||||
if (!li.previousSibling) {
|
||||
goog.dom.insertSiblingBefore(newNode, listNode);
|
||||
} else {
|
||||
if (li.nextSibling) {
|
||||
var listClone = listNode.cloneNode(false);
|
||||
while (li.nextSibling) {
|
||||
listClone.appendChild(li.nextSibling);
|
||||
}
|
||||
goog.dom.insertSiblingAfter(listClone, listNode);
|
||||
}
|
||||
goog.dom.insertSiblingAfter(newNode, listNode);
|
||||
}
|
||||
if (goog.editor.node.isEmpty(listNode)) {
|
||||
goog.dom.removeNode(listNode);
|
||||
}
|
||||
goog.dom.removeNode(li);
|
||||
newNode.innerHTML = ' ';
|
||||
|
||||
return newNode;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Wrap the text indicated by "position" in an HTML container of type
|
||||
* "nodeName".
|
||||
* @param {string} nodeName Type of container, e.g. "p" (paragraph).
|
||||
* @param {Object} position The W3C cursor position object
|
||||
* (from getCursorPositionW3c).
|
||||
* @param {Node} container The field containing position.
|
||||
* @return {Element} The container element that holds the contents from
|
||||
* position.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.wrapInContainerW3c_ = function(nodeName,
|
||||
position, container) {
|
||||
var start = position.node;
|
||||
while (start.previousSibling &&
|
||||
!goog.editor.style.isContainer(start.previousSibling)) {
|
||||
start = start.previousSibling;
|
||||
}
|
||||
|
||||
var end = position.node;
|
||||
while (end.nextSibling &&
|
||||
!goog.editor.style.isContainer(end.nextSibling)) {
|
||||
end = end.nextSibling;
|
||||
}
|
||||
|
||||
var para = container.ownerDocument.createElement(nodeName);
|
||||
while (start != end) {
|
||||
var newStart = start.nextSibling;
|
||||
goog.dom.appendChild(para, start);
|
||||
start = newStart;
|
||||
}
|
||||
var nextSibling = end.nextSibling;
|
||||
goog.dom.appendChild(para, end);
|
||||
container.insertBefore(para, nextSibling);
|
||||
|
||||
return para;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* When we delete an element, FF inserts a BR. We want to strip that
|
||||
* BR after the fact, but in the case where your cursor is at a character
|
||||
* right before a BR and you delete that character, we don't want to
|
||||
* strip it. So we detect this case on keydown and mark the BR as not needing
|
||||
* removal.
|
||||
* @param {goog.dom.AbstractRange} range The closure range object.
|
||||
* @param {boolean} isBackspace Whether this is handling the backspace key.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.markBrToNotBeRemoved_ =
|
||||
function(range, isBackspace) {
|
||||
var focusNode = range.getFocusNode();
|
||||
var focusOffset = range.getFocusOffset();
|
||||
var newEndOffset = isBackspace ? focusOffset : focusOffset + 1;
|
||||
|
||||
if (goog.editor.node.getLength(focusNode) == newEndOffset) {
|
||||
var sibling = focusNode.nextSibling;
|
||||
if (sibling && sibling.tagName == goog.dom.TagName.BR) {
|
||||
this.brToKeep_ = sibling;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If we hit delete/backspace to merge elements, FF inserts a BR.
|
||||
* We want to strip that BR. In markBrToNotBeRemoved, we detect if
|
||||
* there was already a BR there before the delete/backspace so that
|
||||
* we don't accidentally remove a user-inserted BR.
|
||||
* @param {boolean} isBackSpace Whether this is handling the backspace key.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.removeBrIfNecessary_ = function(
|
||||
isBackSpace) {
|
||||
var range = this.getFieldObject().getRange();
|
||||
var focusNode = range.getFocusNode();
|
||||
var focusOffset = range.getFocusOffset();
|
||||
|
||||
var sibling;
|
||||
if (isBackSpace && focusNode.data == '') {
|
||||
// nasty hack. sometimes firefox will backspace a paragraph and put
|
||||
// the cursor before the BR. when it does this, the focusNode is
|
||||
// an empty textnode.
|
||||
sibling = focusNode.nextSibling;
|
||||
} else if (isBackSpace && focusOffset == 0) {
|
||||
var node = focusNode;
|
||||
while (node && !node.previousSibling &&
|
||||
node.parentNode != this.getFieldObject().getElement()) {
|
||||
node = node.parentNode;
|
||||
}
|
||||
sibling = node.previousSibling;
|
||||
} else if (focusNode.length == focusOffset) {
|
||||
sibling = focusNode.nextSibling;
|
||||
}
|
||||
|
||||
if (!sibling || sibling.tagName != goog.dom.TagName.BR ||
|
||||
this.brToKeep_ == sibling) {
|
||||
return;
|
||||
}
|
||||
|
||||
goog.dom.removeNode(sibling);
|
||||
if (focusNode.nodeType == goog.dom.NodeType.TEXT) {
|
||||
// Sometimes firefox inserts extra whitespace. Do our best to deal.
|
||||
// This is buggy though.
|
||||
focusNode.data =
|
||||
goog.editor.plugins.TagOnEnterHandler.trimTabsAndLineBreaks_(
|
||||
focusNode.data);
|
||||
// When we strip whitespace, make sure that our cursor is still at
|
||||
// the end of the textnode.
|
||||
goog.dom.Range.createCaret(focusNode,
|
||||
Math.min(focusOffset, focusNode.length)).select();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Trim the tabs and line breaks from a string.
|
||||
* @param {string} string String to trim.
|
||||
* @return {string} Trimmed string.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.trimTabsAndLineBreaks_ = function(
|
||||
string) {
|
||||
return string.replace(/^[\t\n\r]|[\t\n\r]$/g, '');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Called in response to a normal enter keystroke. It has the action of
|
||||
* splitting elements.
|
||||
* @return {Element} The node that the cursor should be before.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.handleRegularEnterGecko_ =
|
||||
function() {
|
||||
var range = this.getFieldObject().getRange();
|
||||
var container =
|
||||
goog.editor.style.getContainer(range.getContainerElement());
|
||||
var newNode;
|
||||
if (goog.editor.plugins.EnterHandler.isBrElem(container)) {
|
||||
if (container.tagName == goog.dom.TagName.BODY) {
|
||||
// If the field contains only a single BR, this code ensures we don't
|
||||
// try to clone the body tag.
|
||||
container = this.ensureNodeIsWrappedW3c_(
|
||||
container.getElementsByTagName(goog.dom.TagName.BR)[0],
|
||||
container);
|
||||
}
|
||||
|
||||
newNode = container.cloneNode(true);
|
||||
goog.dom.insertSiblingAfter(newNode, container);
|
||||
} else {
|
||||
if (!container.firstChild) {
|
||||
container.innerHTML = ' ';
|
||||
}
|
||||
|
||||
var position = goog.editor.range.getDeepEndPoint(range, true);
|
||||
container = this.ensureNodeIsWrappedW3c_(position.node, container);
|
||||
|
||||
newNode = goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_(
|
||||
position.node, position.offset, container);
|
||||
|
||||
// If the left half and right half of the splitted node are anchors then
|
||||
// that means the user pressed enter while the caret was inside
|
||||
// an anchor tag and split it. The left half is the first anchor
|
||||
// found while traversing the right branch of container. The right half
|
||||
// is the first anchor found while traversing the left branch of newNode.
|
||||
var leftAnchor =
|
||||
goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_(
|
||||
container);
|
||||
var rightAnchor =
|
||||
goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_(
|
||||
newNode, true);
|
||||
if (leftAnchor && rightAnchor &&
|
||||
leftAnchor.tagName == goog.dom.TagName.A &&
|
||||
rightAnchor.tagName == goog.dom.TagName.A) {
|
||||
// If the original anchor (left anchor) is now empty, that means
|
||||
// the user pressed [Enter] at the beginning of the anchor,
|
||||
// in which case we we
|
||||
// want to replace that anchor with its child nodes
|
||||
// Otherwise, we take the second half of the splitted text and break
|
||||
// it out of the anchor.
|
||||
var anchorToRemove = goog.editor.node.isEmpty(leftAnchor, false) ?
|
||||
leftAnchor : rightAnchor;
|
||||
goog.dom.flattenElement(/** @type {Element} */ (anchorToRemove));
|
||||
}
|
||||
}
|
||||
return /** @type {Element} */ (newNode);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Scroll the cursor into view, resulting from splitting the paragraph/adding
|
||||
* a br. It behaves differently than scrollIntoView
|
||||
* @param {Element} element The element immediately following the cursor. Will
|
||||
* be used to determine how to scroll in order to make the cursor visible.
|
||||
* CANNOT be a BR, as they do not have offsetHeight/offsetTop.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.prototype.scrollCursorIntoViewGecko_ =
|
||||
function(element) {
|
||||
if (!this.getFieldObject().isFixedHeight()) {
|
||||
return; // Only need to scroll fixed height fields.
|
||||
}
|
||||
|
||||
var field = this.getFieldObject().getElement();
|
||||
|
||||
// Get the y position of the element we want to scroll to
|
||||
var elementY = goog.style.getPageOffsetTop(element);
|
||||
|
||||
// Determine the height of that element, since we want the bottom of the
|
||||
// element to be in view.
|
||||
var bottomOfNode = elementY + element.offsetHeight;
|
||||
|
||||
var dom = this.getFieldDomHelper();
|
||||
var win = this.getFieldDomHelper().getWindow();
|
||||
var scrollY = dom.getDocumentScroll().y;
|
||||
var viewportHeight = goog.dom.getViewportSize(win).height;
|
||||
|
||||
// If the botom of the element is outside the viewport, move it into view
|
||||
if (bottomOfNode > viewportHeight + scrollY) {
|
||||
// In standards mode, use the html element and not the body
|
||||
if (field.tagName == goog.dom.TagName.BODY &&
|
||||
goog.editor.node.isStandardsMode(field)) {
|
||||
field = field.parentNode;
|
||||
}
|
||||
field.scrollTop = bottomOfNode - viewportHeight;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Splits the DOM tree around the given node and returns the node
|
||||
* containing the second half of the tree. The first half of the tree
|
||||
* is modified, but not removed from the DOM.
|
||||
* @param {Node} positionNode Node to split at.
|
||||
* @param {number} positionOffset Offset into positionNode to split at. If
|
||||
* positionNode is a text node, this offset is an offset in to the text
|
||||
* content of that node. Otherwise, positionOffset is an offset in to
|
||||
* the childNodes array. All elements with child index of positionOffset
|
||||
* or greater will be moved to the second half. If positionNode is an
|
||||
* empty element, the dom will be split at that element, with positionNode
|
||||
* ending up in the second half. positionOffset must be 0 in this case.
|
||||
* @param {Node=} opt_root Node at which to stop splitting the dom (the root
|
||||
* is also split).
|
||||
* @return {Node} The node containing the second half of the tree.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.splitDom_ = function(
|
||||
positionNode, positionOffset, opt_root) {
|
||||
if (!opt_root) opt_root = positionNode.ownerDocument.body;
|
||||
|
||||
// Split the node.
|
||||
var textSplit = positionNode.nodeType == goog.dom.NodeType.TEXT;
|
||||
var secondHalfOfSplitNode;
|
||||
if (textSplit) {
|
||||
if (goog.userAgent.IE &&
|
||||
positionOffset == positionNode.nodeValue.length) {
|
||||
// Since splitText fails in IE at the end of a node, we split it manually.
|
||||
secondHalfOfSplitNode = goog.dom.getDomHelper(positionNode).
|
||||
createTextNode('');
|
||||
goog.dom.insertSiblingAfter(secondHalfOfSplitNode, positionNode);
|
||||
} else {
|
||||
secondHalfOfSplitNode = positionNode.splitText(positionOffset);
|
||||
}
|
||||
} else {
|
||||
// Here we ensure positionNode is the last node in the first half of the
|
||||
// resulting tree.
|
||||
if (positionOffset) {
|
||||
// Use offset as an index in to childNodes.
|
||||
positionNode = positionNode.childNodes[positionOffset - 1];
|
||||
} else {
|
||||
// In this case, positionNode would be the last node in the first half
|
||||
// of the tree, but we actually want to move it to the second half.
|
||||
// Therefore we set secondHalfOfSplitNode to the same node.
|
||||
positionNode = secondHalfOfSplitNode = positionNode.firstChild ||
|
||||
positionNode;
|
||||
}
|
||||
}
|
||||
|
||||
// Create second half of the tree.
|
||||
var secondHalf = goog.editor.node.splitDomTreeAt(
|
||||
positionNode, secondHalfOfSplitNode, opt_root);
|
||||
|
||||
if (textSplit) {
|
||||
// Join secondHalfOfSplitNode and its right text siblings together and
|
||||
// then replace leading NonNbspWhiteSpace with a Nbsp. If
|
||||
// secondHalfOfSplitNode has a right sibling that isn't a text node,
|
||||
// then we can leave secondHalfOfSplitNode empty.
|
||||
secondHalfOfSplitNode =
|
||||
goog.editor.plugins.TagOnEnterHandler.joinTextNodes_(
|
||||
secondHalfOfSplitNode, true);
|
||||
goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
|
||||
secondHalfOfSplitNode, true, !!secondHalfOfSplitNode.nextSibling);
|
||||
|
||||
// Join positionNode and its left text siblings together and then replace
|
||||
// trailing NonNbspWhiteSpace with a Nbsp.
|
||||
var firstHalf = goog.editor.plugins.TagOnEnterHandler.joinTextNodes_(
|
||||
positionNode, false);
|
||||
goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_(
|
||||
firstHalf, false, false);
|
||||
}
|
||||
|
||||
return secondHalf;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Splits the DOM tree around the given node and returns the node containing
|
||||
* second half of the tree, which is appended after the old node. The first
|
||||
* half of the tree is modified, but not removed from the DOM.
|
||||
* @param {Node} positionNode Node to split at.
|
||||
* @param {number} positionOffset Offset into positionNode to split at. If
|
||||
* positionNode is a text node, this offset is an offset in to the text
|
||||
* content of that node. Otherwise, positionOffset is an offset in to
|
||||
* the childNodes array. All elements with child index of positionOffset
|
||||
* or greater will be moved to the second half. If positionNode is an
|
||||
* empty element, the dom will be split at that element, with positionNode
|
||||
* ending up in the second half. positionOffset must be 0 in this case.
|
||||
* @param {Node} node Node to split.
|
||||
* @return {Node} The node containing the second half of the tree.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.splitDomAndAppend_ = function(
|
||||
positionNode, positionOffset, node) {
|
||||
var newNode = goog.editor.plugins.TagOnEnterHandler.splitDom_(
|
||||
positionNode, positionOffset, node);
|
||||
goog.dom.insertSiblingAfter(newNode, node);
|
||||
return newNode;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Joins node and its adjacent text nodes together.
|
||||
* @param {Node} node The node to start joining.
|
||||
* @param {boolean} moveForward Determines whether to join left siblings (false)
|
||||
* or right siblings (true).
|
||||
* @return {Node} The joined text node.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.joinTextNodes_ = function(node,
|
||||
moveForward) {
|
||||
if (node && node.nodeName == '#text') {
|
||||
var nextNodeFn = moveForward ? 'nextSibling' : 'previousSibling';
|
||||
var prevNodeFn = moveForward ? 'previousSibling' : 'nextSibling';
|
||||
var nodeValues = [node.nodeValue];
|
||||
while (node[nextNodeFn] &&
|
||||
node[nextNodeFn].nodeType == goog.dom.NodeType.TEXT) {
|
||||
node = node[nextNodeFn];
|
||||
nodeValues.push(node.nodeValue);
|
||||
goog.dom.removeNode(node[prevNodeFn]);
|
||||
}
|
||||
if (!moveForward) {
|
||||
nodeValues.reverse();
|
||||
}
|
||||
node.nodeValue = nodeValues.join('');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replaces leading or trailing spaces of a text node to a single Nbsp.
|
||||
* @param {Node} textNode The text node to search and replace white spaces.
|
||||
* @param {boolean} fromStart Set to true to replace leading spaces, false to
|
||||
* replace trailing spaces.
|
||||
* @param {boolean} isLeaveEmpty Set to true to leave the node empty if the
|
||||
* text node was empty in the first place, otherwise put a Nbsp into the
|
||||
* text node.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.replaceWhiteSpaceWithNbsp_ = function(
|
||||
textNode, fromStart, isLeaveEmpty) {
|
||||
var regExp = fromStart ? /^[ \t\r\n]+/ : /[ \t\r\n]+$/;
|
||||
textNode.nodeValue = textNode.nodeValue.replace(regExp,
|
||||
goog.string.Unicode.NBSP);
|
||||
|
||||
if (!isLeaveEmpty && textNode.nodeValue == '') {
|
||||
textNode.nodeValue = goog.string.Unicode.NBSP;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Finds the first A element in a traversal from the input node. The input
|
||||
* node itself is not included in the search.
|
||||
* @param {Node} node The node to start searching from.
|
||||
* @param {boolean=} opt_useFirstChild Whether to traverse along the first child
|
||||
* (true) or last child (false).
|
||||
* @return {Node} The first anchor node found in the search, or null if none
|
||||
* was found.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.TagOnEnterHandler.findAnchorInTraversal_ = function(node,
|
||||
opt_useFirstChild) {
|
||||
while ((node = opt_useFirstChild ? node.firstChild : node.lastChild) &&
|
||||
node.tagName != goog.dom.TagName.A) {
|
||||
// Do nothing - advancement is handled in the condition.
|
||||
}
|
||||
return node;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Code for managing series of undo-redo actions in the form of
|
||||
* {@link goog.editor.plugins.UndoRedoState}s.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.editor.plugins.UndoRedoManager');
|
||||
goog.provide('goog.editor.plugins.UndoRedoManager.EventType');
|
||||
|
||||
goog.require('goog.editor.plugins.UndoRedoState');
|
||||
goog.require('goog.events.EventTarget');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Manages undo and redo operations through a series of {@code UndoRedoState}s
|
||||
* maintained on undo and redo stacks.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager = function() {
|
||||
goog.events.EventTarget.call(this);
|
||||
|
||||
/**
|
||||
* The maximum number of states on the undo stack at any time. Used to limit
|
||||
* the memory footprint of the undo-redo stack.
|
||||
* TODO(user) have a separate memory size based limit.
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.maxUndoDepth_ = 100;
|
||||
|
||||
/**
|
||||
* The undo stack.
|
||||
* @type {Array.<goog.editor.plugins.UndoRedoState>}
|
||||
* @private
|
||||
*/
|
||||
this.undoStack_ = [];
|
||||
|
||||
/**
|
||||
* The redo stack.
|
||||
* @type {Array.<goog.editor.plugins.UndoRedoState>}
|
||||
* @private
|
||||
*/
|
||||
this.redoStack_ = [];
|
||||
|
||||
/**
|
||||
* A queue of pending undo or redo actions. Stored as objects with two
|
||||
* properties: func and state. The func property stores the undo or redo
|
||||
* function to be called, the state property stores the state that method
|
||||
* came from.
|
||||
* @type {Array.<Object>}
|
||||
* @private
|
||||
*/
|
||||
this.pendingActions_ = [];
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.UndoRedoManager, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* Event types for the events dispatched by undo-redo manager.
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.EventType = {
|
||||
/**
|
||||
* Signifies that he undo or redo stack transitioned between 0 and 1 states,
|
||||
* meaning that the ability to peform undo or redo operations has changed.
|
||||
*/
|
||||
STATE_CHANGE: 'state_change',
|
||||
|
||||
/**
|
||||
* Signifies that a state was just added to the undo stack. Events of this
|
||||
* type will have a {@code state} property whose value is the state that
|
||||
* was just added.
|
||||
*/
|
||||
STATE_ADDED: 'state_added',
|
||||
|
||||
/**
|
||||
* Signifies that the undo method of a state is about to be called.
|
||||
* Events of this type will have a {@code state} property whose value is the
|
||||
* state whose undo action is about to be performed. If the event is cancelled
|
||||
* the action does not proceed, but the state will still transition between
|
||||
* stacks.
|
||||
*/
|
||||
BEFORE_UNDO: 'before_undo',
|
||||
|
||||
/**
|
||||
* Signifies that the redo method of a state is about to be called.
|
||||
* Events of this type will have a {@code state} property whose value is the
|
||||
* state whose redo action is about to be performed. If the event is cancelled
|
||||
* the action does not proceed, but the state will still transition between
|
||||
* stacks.
|
||||
*/
|
||||
BEFORE_REDO: 'before_redo'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The key for the listener for the completion of the asynchronous state whose
|
||||
* undo or redo action is in progress. Null if no action is in progress.
|
||||
* @type {goog.events.Key}
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.inProgressActionKey_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Set the max undo stack depth (not the real memory usage).
|
||||
* @param {number} depth Depth of the stack.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.setMaxUndoDepth =
|
||||
function(depth) {
|
||||
this.maxUndoDepth_ = depth;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add state to the undo stack. This clears the redo stack.
|
||||
*
|
||||
* @param {goog.editor.plugins.UndoRedoState} state The state to add to the undo
|
||||
* stack.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.addState = function(state) {
|
||||
// TODO: is the state.equals check necessary?
|
||||
if (this.undoStack_.length == 0 ||
|
||||
!state.equals(this.undoStack_[this.undoStack_.length - 1])) {
|
||||
this.undoStack_.push(state);
|
||||
if (this.undoStack_.length > this.maxUndoDepth_) {
|
||||
this.undoStack_.shift();
|
||||
}
|
||||
// Clobber the redo stack.
|
||||
var redoLength = this.redoStack_.length;
|
||||
this.redoStack_.length = 0;
|
||||
|
||||
this.dispatchEvent({
|
||||
type: goog.editor.plugins.UndoRedoManager.EventType.STATE_ADDED,
|
||||
state: state
|
||||
});
|
||||
|
||||
// If the redo state had states on it, then clobbering the redo stack above
|
||||
// has caused a state change.
|
||||
if (this.undoStack_.length == 1 || redoLength) {
|
||||
this.dispatchStateChange_();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dispatches a STATE_CHANGE event with this manager as the target.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.dispatchStateChange_ =
|
||||
function() {
|
||||
this.dispatchEvent(
|
||||
goog.editor.plugins.UndoRedoManager.EventType.STATE_CHANGE);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Performs the undo operation of the state at the top of the undo stack, moving
|
||||
* that state to the top of the redo stack. If the undo stack is empty, does
|
||||
* nothing.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.undo = function() {
|
||||
this.shiftState_(this.undoStack_, this.redoStack_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Performs the redo operation of the state at the top of the redo stack, moving
|
||||
* that state to the top of the undo stack. If redo undo stack is empty, does
|
||||
* nothing.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.redo = function() {
|
||||
this.shiftState_(this.redoStack_, this.undoStack_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Wether the undo stack has items on it, i.e., if it is
|
||||
* possible to perform an undo operation.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.hasUndoState = function() {
|
||||
return this.undoStack_.length > 0;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Wether the redo stack has items on it, i.e., if it is
|
||||
* possible to perform a redo operation.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.hasRedoState = function() {
|
||||
return this.redoStack_.length > 0;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Move a state from one stack to the other, performing the appropriate undo
|
||||
* or redo action.
|
||||
*
|
||||
* @param {Array.<goog.editor.plugins.UndoRedoState>} fromStack Stack to move
|
||||
* the state from.
|
||||
* @param {Array.<goog.editor.plugins.UndoRedoState>} toStack Stack to move
|
||||
* the state to.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.shiftState_ = function(
|
||||
fromStack, toStack) {
|
||||
if (fromStack.length) {
|
||||
var state = fromStack.pop();
|
||||
|
||||
// Push the current state into the redo stack.
|
||||
toStack.push(state);
|
||||
|
||||
this.addAction_({
|
||||
type: fromStack == this.undoStack_ ?
|
||||
goog.editor.plugins.UndoRedoManager.EventType.BEFORE_UNDO :
|
||||
goog.editor.plugins.UndoRedoManager.EventType.BEFORE_REDO,
|
||||
func: fromStack == this.undoStack_ ? state.undo : state.redo,
|
||||
state: state
|
||||
});
|
||||
|
||||
// If either stack transitioned between 0 and 1 in size then the ability
|
||||
// to do an undo or redo has changed and we must dispatch a state change.
|
||||
if (fromStack.length == 0 || toStack.length == 1) {
|
||||
this.dispatchStateChange_();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Adds an action to the queue of pending undo or redo actions. If no actions
|
||||
* are pending, immediately performs the action.
|
||||
*
|
||||
* @param {Object} action An undo or redo action. Stored as an object with two
|
||||
* properties: func and state. The func property stores the undo or redo
|
||||
* function to be called, the state property stores the state that method
|
||||
* came from.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.addAction_ = function(action) {
|
||||
this.pendingActions_.push(action);
|
||||
if (this.pendingActions_.length == 1) {
|
||||
this.doAction_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Executes the action at the front of the pending actions queue. If an action
|
||||
* is already in progress or the queue is empty, does nothing.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.doAction_ = function() {
|
||||
if (this.inProgressActionKey_ || this.pendingActions_.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var action = this.pendingActions_.shift();
|
||||
|
||||
var e = {
|
||||
type: action.type,
|
||||
state: action.state
|
||||
};
|
||||
|
||||
if (this.dispatchEvent(e)) {
|
||||
if (action.state.isAsynchronous()) {
|
||||
this.inProgressActionKey_ = goog.events.listen(action.state,
|
||||
goog.editor.plugins.UndoRedoState.ACTION_COMPLETED,
|
||||
this.finishAction_, false, this);
|
||||
action.func.call(action.state);
|
||||
} else {
|
||||
action.func.call(action.state);
|
||||
this.doAction_();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Finishes processing the current in progress action, starting the next queued
|
||||
* action if one exists.
|
||||
* @private
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.finishAction_ = function() {
|
||||
goog.events.unlistenByKey(/** @type {number} */ (this.inProgressActionKey_));
|
||||
this.inProgressActionKey_ = null;
|
||||
this.doAction_();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the undo and redo stacks.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.clearHistory = function() {
|
||||
if (this.undoStack_.length > 0 || this.redoStack_.length > 0) {
|
||||
this.undoStack_.length = 0;
|
||||
this.redoStack_.length = 0;
|
||||
this.dispatchStateChange_();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.editor.plugins.UndoRedoState|undefined} The state at the top of
|
||||
* the undo stack without removing it from the stack.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.undoPeek = function() {
|
||||
return this.undoStack_[this.undoStack_.length - 1];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.editor.plugins.UndoRedoState|undefined} The state at the top of
|
||||
* the redo stack without removing it from the stack.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoManager.prototype.redoPeek = function() {
|
||||
return this.redoStack_[this.redoStack_.length - 1];
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* @fileoverview Code for an UndoRedoState interface representing an undo and
|
||||
* redo action for a particular state change. To be used by
|
||||
* {@link goog.editor.plugins.UndoRedoManager}.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.editor.plugins.UndoRedoState');
|
||||
|
||||
goog.require('goog.events.EventTarget');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Represents an undo and redo action for a particular state transition.
|
||||
*
|
||||
* @param {boolean} asynchronous Whether the undo or redo actions for this
|
||||
* state complete asynchronously. If true, then this state must fire
|
||||
* an ACTION_COMPLETED event when undo or redo is complete.
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoState = function(asynchronous) {
|
||||
goog.base(this);
|
||||
|
||||
/**
|
||||
* Indicates if the undo or redo actions for this state complete
|
||||
* asynchronously.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.asynchronous_ = asynchronous;
|
||||
};
|
||||
goog.inherits(goog.editor.plugins.UndoRedoState, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* Event type for events indicating that this state has completed an undo or
|
||||
* redo operation.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoState.ACTION_COMPLETED = 'action_completed';
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether or not the undo and redo actions of this state
|
||||
* complete asynchronously. If true, the state will fire an ACTION_COMPLETED
|
||||
* event when an undo or redo action is complete.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoState.prototype.isAsynchronous = function() {
|
||||
return this.asynchronous_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Undoes the action represented by this state.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoState.prototype.undo = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Redoes the action represented by this state.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoState.prototype.redo = goog.abstractMethod;
|
||||
|
||||
|
||||
/**
|
||||
* Checks if two undo-redo states are the same.
|
||||
* @param {goog.editor.plugins.UndoRedoState} state The state to compare.
|
||||
* @return {boolean} Wether the two states are equal.
|
||||
*/
|
||||
goog.editor.plugins.UndoRedoState.prototype.equals = goog.abstractMethod;
|
||||
Reference in New Issue
Block a user