Update wmts-hidpi, add nicer-api-docs

This commit is contained in:
Andreas Hocevar
2014-05-06 13:02:46 -05:00
parent b3ac1afd00
commit 1e25fc5585
2239 changed files with 3726515 additions and 37010 deletions

View File

@@ -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;
};

View File

@@ -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;
}
};

View File

@@ -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

View File

@@ -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 = '&nbsp;'; // 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 = '&nbsp;';
// 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>&nbsp;</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;
};

View File

@@ -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);
}
};

View File

@@ -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 = '&nbsp;';
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);
};

View File

@@ -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);
};

View File

@@ -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();
};

View File

@@ -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. &lt;div&gt;) or a whole list (e.g. &lt;ul&gt;).
*
* 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));
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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());
};

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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();
}
}
}
};

View File

@@ -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 &nbsp;.
var nbspRegExp =
goog.userAgent.isVersionOrHigher('528') ? /&nbsp;/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 &nbsp; 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;
};

View File

@@ -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;
};

View File

@@ -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]);
};

View File

@@ -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>&nbsp;</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[^>]*\/?>|&nbsp;)[\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>&nbsp;</li>...</ul>
// 2. <ul id='foo1'>...<li id='foo2'>&nbsp;</li>...</ul>
// 3. <ul id='foo1'>...</ul><p id='foo3'>&nbsp;</p><ul id='foo2'>...</ul>
// 4. <ul>...</ul><p>&nbsp;</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 = '&nbsp;';
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 = '&nbsp;';
}
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

View File

@@ -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];
};

View File

@@ -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;