768 lines
27 KiB
JavaScript
768 lines
27 KiB
JavaScript
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS-IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/**
|
|
* @fileoverview Plugin to handle enter keys.
|
|
*
|
|
*/
|
|
|
|
goog.provide('goog.editor.plugins.EnterHandler');
|
|
|
|
goog.require('goog.dom');
|
|
goog.require('goog.dom.NodeOffset');
|
|
goog.require('goog.dom.NodeType');
|
|
goog.require('goog.dom.Range');
|
|
goog.require('goog.dom.TagName');
|
|
goog.require('goog.editor.BrowserFeature');
|
|
goog.require('goog.editor.Plugin');
|
|
goog.require('goog.editor.node');
|
|
goog.require('goog.editor.plugins.Blockquote');
|
|
goog.require('goog.editor.range');
|
|
goog.require('goog.editor.style');
|
|
goog.require('goog.events.KeyCodes');
|
|
goog.require('goog.functions');
|
|
goog.require('goog.object');
|
|
goog.require('goog.string');
|
|
goog.require('goog.userAgent');
|
|
|
|
|
|
|
|
/**
|
|
* Plugin to handle enter keys. This does all the crazy to normalize (as much as
|
|
* is reasonable) what happens when you hit enter. This also handles the
|
|
* special casing of hitting enter in a blockquote.
|
|
*
|
|
* In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In
|
|
* Firefox, the resulting HTML uses BR tags at the end of each line.
|
|
*
|
|
* @constructor
|
|
* @extends {goog.editor.Plugin}
|
|
*/
|
|
goog.editor.plugins.EnterHandler = function() {
|
|
goog.editor.Plugin.call(this);
|
|
};
|
|
goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);
|
|
|
|
|
|
/**
|
|
* The type of block level tag to add on enter, for browsers that support
|
|
* specifying the default block-level tag. Can be overriden by subclasses; must
|
|
* be either DIV or P.
|
|
* @type {goog.dom.TagName}
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {
|
|
return 'EnterHandler';
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {
|
|
goog.base(this, 'enable', fieldObject);
|
|
|
|
if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&
|
|
(this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {
|
|
var doc = this.getFieldDomHelper().getDocument();
|
|
doc.execCommand('opera-defaultBlock', false, this.tag);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* If the contents are empty, return the 'default' html for the field.
|
|
* The 'default' contents depend on the enter handling mode, so it
|
|
* makes the most sense in this plugin.
|
|
* @param {string} html The html to prepare.
|
|
* @return {string} The original HTML, or default contents if that
|
|
* html is empty.
|
|
* @override
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(
|
|
html) {
|
|
if (!html || goog.string.isBreakingWhitespace(html)) {
|
|
return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?
|
|
this.getNonCollapsingBlankHtml() : '';
|
|
}
|
|
return html;
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets HTML with no contents that won't collapse, for browsers that
|
|
* collapse the empty string.
|
|
* @return {string} Blank html.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =
|
|
goog.functions.constant('<br>');
|
|
|
|
|
|
/**
|
|
* Internal backspace handler.
|
|
* @param {goog.events.Event} e The keypress event.
|
|
* @param {goog.dom.AbstractRange} range The closure range object.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(e,
|
|
range) {
|
|
var field = this.getFieldObject().getElement();
|
|
var container = range && range.getStartNode();
|
|
|
|
if (field.firstChild == container && goog.editor.node.isEmpty(container)) {
|
|
e.preventDefault();
|
|
// TODO(user): I think we probably don't need to stopPropagation here
|
|
e.stopPropagation();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Fix paragraphs to be the correct type of node.
|
|
* @param {goog.events.Event} e The <enter> key event.
|
|
* @param {boolean} split Whether we already split up a blockquote by
|
|
* manually inserting elements.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =
|
|
function(e, split) {
|
|
// Force IE to turn the node we are leaving into a DIV. If we do turn
|
|
// it into a DIV, the node IE creates in response to ENTER will also be
|
|
// a DIV. If we don't, it will be a P. We handle that case
|
|
// in handleKeyUpIE_
|
|
if (goog.userAgent.IE || goog.userAgent.OPERA) {
|
|
this.ensureBlockIeOpera(goog.dom.TagName.DIV);
|
|
} else if (!split && goog.userAgent.WEBKIT) {
|
|
// WebKit duplicates a blockquote when the user hits enter. Let's cancel
|
|
// this and insert a BR instead, to make it more consistent with the other
|
|
// browsers.
|
|
var range = this.getFieldObject().getRange();
|
|
if (!range || !goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(
|
|
range.getContainerElement())) {
|
|
return;
|
|
}
|
|
|
|
var dh = this.getFieldDomHelper();
|
|
var br = dh.createElement(goog.dom.TagName.BR);
|
|
range.insertNode(br, true);
|
|
|
|
// If the BR is at the end of a block element, Safari still thinks there is
|
|
// only one line instead of two, so we need to add another BR in that case.
|
|
if (goog.editor.node.isBlockTag(br.parentNode) &&
|
|
!goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {
|
|
goog.dom.insertSiblingBefore(
|
|
dh.createElement(goog.dom.TagName.BR), br);
|
|
}
|
|
|
|
goog.editor.range.placeCursorNextTo(br, false);
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Determines whether the lowest containing block node is a blockquote.
|
|
* @param {Node} n The node.
|
|
* @return {boolean} Whether the deepest block ancestor of n is a blockquote.
|
|
*/
|
|
goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {
|
|
for (var current = n; current; current = current.parentNode) {
|
|
if (goog.editor.node.isBlockTag(current)) {
|
|
return current.tagName == goog.dom.TagName.BLOCKQUOTE;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Internal delete key handler.
|
|
* @param {goog.events.Event} e The keypress event.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {
|
|
this.deleteBrGecko(e);
|
|
};
|
|
|
|
|
|
/**
|
|
* Deletes the element at the cursor if it is a BR node, and if it does, calls
|
|
* e.preventDefault to stop the browser from deleting. Only necessary in Gecko
|
|
* as a workaround for mozilla bug 205350 where deleting a BR that is followed
|
|
* by a block element doesn't work (the BR gets immediately replaced). We also
|
|
* need to account for an ill-formed cursor which occurs from us trying to
|
|
* stop the browser from deleting.
|
|
*
|
|
* @param {goog.events.Event} e The DELETE keypress event.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {
|
|
var range = this.getFieldObject().getRange();
|
|
if (range.isCollapsed()) {
|
|
var container = range.getEndNode();
|
|
if (container.nodeType == goog.dom.NodeType.ELEMENT) {
|
|
var nextNode = container.childNodes[range.getEndOffset()];
|
|
if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {
|
|
// We want to retrieve the first non-whitespace previous sibling
|
|
// as we could have added an empty text node below and want to
|
|
// properly handle deleting a sequence of BR's.
|
|
var previousSibling = goog.editor.node.getPreviousSibling(nextNode);
|
|
var nextSibling = nextNode.nextSibling;
|
|
|
|
container.removeChild(nextNode);
|
|
e.preventDefault();
|
|
|
|
// When we delete a BR followed by a block level element, the cursor
|
|
// has a line-height which spans the height of the block level element.
|
|
// e.g. If we delete a BR followed by a UL, the resulting HTML will
|
|
// appear to the end user like:-
|
|
//
|
|
// | * one
|
|
// | * two
|
|
// | * three
|
|
//
|
|
// There are a couple of cases that we have to account for in order to
|
|
// properly conform to what the user expects when DELETE is pressed.
|
|
//
|
|
// 1. If the BR has a previous sibling and the previous sibling is
|
|
// not a block level element or a BR, we place the cursor at the
|
|
// end of that.
|
|
// 2. If the BR doesn't have a previous sibling or the previous sibling
|
|
// is a block level element or a BR, we place the cursor at the
|
|
// beginning of the leftmost leaf of its next sibling.
|
|
if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {
|
|
if (previousSibling &&
|
|
!(previousSibling.tagName == goog.dom.TagName.BR ||
|
|
goog.editor.node.isBlockTag(previousSibling))) {
|
|
goog.dom.Range.createCaret(
|
|
previousSibling,
|
|
goog.editor.node.getLength(previousSibling)).select();
|
|
} else {
|
|
var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);
|
|
goog.dom.Range.createCaret(leftMostLeaf, 0).select();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {
|
|
// If a dialog doesn't have selectable field, Gecko grabs the event and
|
|
// performs actions in editor window. This solves that problem and allows
|
|
// the event to be passed on to proper handlers.
|
|
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
|
|
return false;
|
|
}
|
|
|
|
// Firefox will allow the first node in an iframe to be deleted
|
|
// on a backspace. Disallow it if the node is empty.
|
|
if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
|
|
this.handleBackspaceInternal(e, this.getFieldObject().getRange());
|
|
|
|
} else if (e.keyCode == goog.events.KeyCodes.ENTER) {
|
|
if (goog.userAgent.GECKO) {
|
|
if (!e.shiftKey) {
|
|
// Behave similarly to IE's content editable return carriage:
|
|
// If the shift key is down or specified by the application, insert a
|
|
// BR, otherwise split paragraphs
|
|
this.handleEnterGecko_(e);
|
|
}
|
|
} else {
|
|
// In Gecko-based browsers, this is handled in the handleEnterGecko_
|
|
// method.
|
|
this.getFieldObject().dispatchBeforeChange();
|
|
var cursorPosition = this.deleteCursorSelection_();
|
|
|
|
var split = !!this.getFieldObject().execCommand(
|
|
goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
|
|
if (split) {
|
|
// TODO(user): I think we probably don't need to stopPropagation here
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
this.releasePositionObject_(cursorPosition);
|
|
|
|
if (goog.userAgent.WEBKIT) {
|
|
this.handleEnterWebkitInternal(e);
|
|
}
|
|
|
|
this.processParagraphTagsInternal(e, split);
|
|
this.getFieldObject().dispatchChange();
|
|
}
|
|
|
|
} else if (goog.userAgent.GECKO && e.keyCode == goog.events.KeyCodes.DELETE) {
|
|
this.handleDeleteGecko(e);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {
|
|
// If a dialog doesn't have selectable field, Gecko grabs the event and
|
|
// performs actions in editor window. This solves that problem and allows
|
|
// the event to be passed on to proper handlers.
|
|
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
|
|
return false;
|
|
}
|
|
this.handleKeyUpInternal(e);
|
|
return false;
|
|
};
|
|
|
|
|
|
/**
|
|
* Internal handler for keyup events.
|
|
* @param {goog.events.Event} e The key event.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {
|
|
if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
|
|
e.keyCode == goog.events.KeyCodes.ENTER) {
|
|
this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Handles an enter keypress event on fields in Gecko.
|
|
* @param {goog.events.BrowserEvent} e The key event.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {
|
|
// Retrieve whether the selection is collapsed before we delete it.
|
|
var range = this.getFieldObject().getRange();
|
|
var wasCollapsed = !range || range.isCollapsed();
|
|
var cursorPosition = this.deleteCursorSelection_();
|
|
|
|
var handled = this.getFieldObject().execCommand(
|
|
goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
|
|
if (handled) {
|
|
// TODO(user): I think we probably don't need to stopPropagation here
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
this.releasePositionObject_(cursorPosition);
|
|
if (!handled) {
|
|
this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Handle an enter key press in WebKit.
|
|
* @param {goog.events.BrowserEvent} e The key press event.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =
|
|
goog.nullFunction;
|
|
|
|
|
|
/**
|
|
* Handle an enter key press on collapsed selection. handleEnterGecko_ ensures
|
|
* the selection is collapsed by deleting its contents if it is not. The
|
|
* default implementation does nothing.
|
|
* @param {goog.events.BrowserEvent} e The key press event.
|
|
* @param {boolean} wasCollapsed Whether the selection was collapsed before
|
|
* the key press. If it was not, code before this function has already
|
|
* cleared the contents of the selection.
|
|
* @param {goog.dom.AbstractRange} range Object representing the selection.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =
|
|
goog.nullFunction;
|
|
|
|
|
|
/**
|
|
* Names of all the nodes that we don't want to turn into block nodes in IE when
|
|
* the user hits enter.
|
|
* @type {Object}
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =
|
|
goog.object.createSet(
|
|
goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,
|
|
goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,
|
|
goog.dom.TagName.H5, goog.dom.TagName.H6);
|
|
|
|
|
|
/**
|
|
* Whether this is a node that contains a single BR tag and non-nbsp
|
|
* whitespace.
|
|
* @param {Node} node Node to check.
|
|
* @return {boolean} Whether this is an element that only contains a BR.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.isBrElem = function(node) {
|
|
return goog.editor.node.isEmpty(node) &&
|
|
node.getElementsByTagName(goog.dom.TagName.BR).length == 1;
|
|
};
|
|
|
|
|
|
/**
|
|
* Ensures all text in IE and Opera to be in the given tag in order to control
|
|
* Enter spacing. Call this when Enter is pressed if desired.
|
|
*
|
|
* We want to make sure the user is always inside of a block (or other nodes
|
|
* listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_). We
|
|
* listen to keypress to force nodes that the user is leaving to turn into
|
|
* blocks, but we also need to listen to keyup to force nodes that the user is
|
|
* entering to turn into blocks.
|
|
* Example: html is: "<h2>foo[cursor]</h2>", and the user hits enter. We
|
|
* don't want to format the h2, but we do want to format the P that is
|
|
* created on enter. The P node is not available until keyup.
|
|
* @param {goog.dom.TagName} tag The tag name to convert to.
|
|
* @param {boolean=} opt_keyUp Whether the function is being called on key up.
|
|
* When called on key up, the cursor is in the newly created node, so the
|
|
* semantics for when to change it to a block are different. Specifically,
|
|
* if the resulting node contains only a BR, it is converted to <tag>.
|
|
* @protected
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(tag,
|
|
opt_keyUp) {
|
|
var range = this.getFieldObject().getRange();
|
|
var container = range.getContainer();
|
|
var field = this.getFieldObject().getElement();
|
|
|
|
var paragraph;
|
|
while (container && container != field) {
|
|
// We don't need to ensure a block if we are already in the same block, or
|
|
// in another block level node that we don't want to change the format of
|
|
// (unless we're handling keyUp and that block node just contains a BR).
|
|
var nodeName = container.nodeName;
|
|
// Due to @bug 2455389, the call to isBrElem needs to be inlined in the if
|
|
// instead of done before and saved in a variable, so that it can be
|
|
// short-circuited and avoid a weird IE edge case.
|
|
if (nodeName == tag ||
|
|
(goog.editor.plugins.EnterHandler.
|
|
DO_NOT_ENSURE_BLOCK_NODES_[nodeName] && !(opt_keyUp &&
|
|
goog.editor.plugins.EnterHandler.isBrElem(container)))) {
|
|
// Opera can create a <p> inside of a <div> in some situations,
|
|
// such as when breaking out of a list that is contained in a <div>.
|
|
if (goog.userAgent.OPERA && paragraph) {
|
|
if (nodeName == tag &&
|
|
paragraph == container.lastChild &&
|
|
goog.editor.node.isEmpty(paragraph)) {
|
|
goog.dom.insertSiblingAfter(paragraph, container);
|
|
goog.dom.Range.createFromNodeContents(paragraph).select();
|
|
}
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&
|
|
nodeName != tag) {
|
|
paragraph = container;
|
|
}
|
|
|
|
container = container.parentNode;
|
|
}
|
|
|
|
|
|
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {
|
|
// IE (before IE9) has a bug where if the cursor is directly before a block
|
|
// node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),
|
|
// the FormatBlock command actually formats the "bar" instead of the "foo".
|
|
// This is just wrong. To work-around this, we want to move the
|
|
// selection back one character, and then restore it to its prior position.
|
|
// NOTE: We use the following "range math" to detect this situation because
|
|
// using Closure ranges here triggers a bug in IE that causes a crash.
|
|
// parent2 != parent3 ensures moving the cursor forward one character
|
|
// crosses at least 1 element boundary, and therefore tests if the cursor is
|
|
// at such a boundary. The second check, parent3 != range.parentElement()
|
|
// weeds out some cases where the elements are siblings instead of cousins.
|
|
var needsHelp = false;
|
|
range = range.getBrowserRangeObject();
|
|
var range2 = range.duplicate();
|
|
range2.moveEnd('character', 1);
|
|
// In whitebox mode, when the cursor is at the end of the field, trying to
|
|
// move the end of the range will do nothing, and hence the range's text
|
|
// will be empty. In this case, the cursor clearly isn't sitting just
|
|
// before a block node, since it isn't before anything.
|
|
if (range2.text.length) {
|
|
var parent2 = range2.parentElement();
|
|
|
|
var range3 = range2.duplicate();
|
|
range3.collapse(false);
|
|
var parent3 = range3.parentElement();
|
|
|
|
if ((needsHelp = parent2 != parent3 &&
|
|
parent3 != range.parentElement())) {
|
|
range.move('character', -1);
|
|
range.select();
|
|
}
|
|
}
|
|
}
|
|
|
|
this.getFieldObject().getEditableDomHelper().getDocument().execCommand(
|
|
'FormatBlock', false, '<' + tag + '>');
|
|
|
|
if (needsHelp) {
|
|
range.move('character', 1);
|
|
range.select();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Deletes the content at the current cursor position.
|
|
* @return {Node|Object} Something representing the current cursor position.
|
|
* See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.
|
|
* Should be passed to releasePositionObject_ when no longer in use.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {
|
|
return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
|
|
this.deleteCursorSelectionW3C_() : this.deleteCursorSelectionIE_();
|
|
};
|
|
|
|
|
|
/**
|
|
* Releases the object returned by deleteCursorSelection_.
|
|
* @param {Node|Object} position The object returned by deleteCursorSelection_.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ =
|
|
function(position) {
|
|
if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {
|
|
(/** @type {Node} */ (position)).removeNode(true);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete the selection at the current cursor position, then returns a temporary
|
|
* node at the current position.
|
|
* @return {Node} A temporary node marking the current cursor position. This
|
|
* node should eventually be removed from the DOM.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =
|
|
function() {
|
|
var doc = this.getFieldDomHelper().getDocument();
|
|
var range = doc.selection.createRange();
|
|
|
|
var id = goog.string.createUniqueString();
|
|
range.pasteHTML('<span id="' + id + '"></span>');
|
|
var splitNode = doc.getElementById(id);
|
|
splitNode.id = '';
|
|
return splitNode;
|
|
};
|
|
|
|
|
|
/**
|
|
* Delete the selection at the current cursor position, then returns the node
|
|
* at the current position.
|
|
* @return {goog.editor.range.Point} The current cursor position. Note that
|
|
* unlike simulateEnterIE_, this should not be removed from the DOM.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =
|
|
function() {
|
|
var range = this.getFieldObject().getRange();
|
|
|
|
// Delete the current selection if it's is non-collapsed.
|
|
// Although this is redundant in FF, it's necessary for Safari
|
|
if (!range.isCollapsed()) {
|
|
var shouldDelete = true;
|
|
// Opera selects the <br> in an empty block if there is no text node
|
|
// preceding it. To preserve inline formatting when pressing [enter] inside
|
|
// an empty block, don't delete the selection if it only selects a <br> at
|
|
// the end of the block.
|
|
// TODO(user): Move this into goog.dom.Range. It should detect this state
|
|
// when creating a range from the window selection and fix it in the created
|
|
// range.
|
|
if (goog.userAgent.OPERA) {
|
|
var startNode = range.getStartNode();
|
|
var startOffset = range.getStartOffset();
|
|
if (startNode == range.getEndNode() &&
|
|
// This weeds out cases where startNode is a text node.
|
|
startNode.lastChild &&
|
|
startNode.lastChild.tagName == goog.dom.TagName.BR &&
|
|
// If this check is true, then endOffset is implied to be
|
|
// startOffset + 1, because the selection is not collapsed and
|
|
// it starts and ends within the same element.
|
|
startOffset == startNode.childNodes.length - 1) {
|
|
shouldDelete = false;
|
|
}
|
|
}
|
|
if (shouldDelete) {
|
|
goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
|
|
}
|
|
}
|
|
|
|
return goog.editor.range.getDeepEndPoint(range, true);
|
|
};
|
|
|
|
|
|
/**
|
|
* Deletes the contents of the selection from the DOM.
|
|
* @param {goog.dom.AbstractRange} range The range to remove contents from.
|
|
* @return {goog.dom.AbstractRange} The resulting range. Used for testing.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {
|
|
if (range && !range.isCollapsed()) {
|
|
var reselect = true;
|
|
var baseNode = range.getContainerElement();
|
|
var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);
|
|
var rangeOffset = range.getStartOffset();
|
|
|
|
// Whether the selection crosses no container boundaries.
|
|
var isInOneContainer =
|
|
goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);
|
|
|
|
// Whether the selection ends in a container it doesn't fully select.
|
|
var isPartialEnd = !isInOneContainer &&
|
|
goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);
|
|
|
|
// Remove The range contents, and ensure the correct content stays selected.
|
|
range.removeContents();
|
|
var node = nodeOffset.findTargetNode(baseNode);
|
|
if (node) {
|
|
range = goog.dom.Range.createCaret(node, rangeOffset);
|
|
} else {
|
|
// This occurs when the node that would have been referenced has now been
|
|
// deleted and there are no other nodes in the baseNode. Thus need to
|
|
// set the caret to the end of the base node.
|
|
range =
|
|
goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);
|
|
reselect = false;
|
|
}
|
|
range.select();
|
|
|
|
// If we just deleted everything from the container, add an nbsp
|
|
// to the container, and leave the cursor inside of it
|
|
if (isInOneContainer) {
|
|
var container = goog.editor.style.getContainer(range.getStartNode());
|
|
if (goog.editor.node.isEmpty(container, true)) {
|
|
var html = ' ';
|
|
if (goog.userAgent.OPERA &&
|
|
container.tagName == goog.dom.TagName.LI) {
|
|
// Don't break Opera's native break-out-of-lists behavior.
|
|
html = '<br>';
|
|
}
|
|
goog.editor.node.replaceInnerHtml(container, html);
|
|
goog.editor.range.selectNodeStart(container.firstChild);
|
|
reselect = false;
|
|
}
|
|
}
|
|
|
|
if (isPartialEnd) {
|
|
/*
|
|
This code handles the following, where | is the cursor:
|
|
<div>a|b</div><div>c|d</div>
|
|
After removeContents, the remaining HTML is
|
|
<div>a</div><div>d</div>
|
|
which means the line break between the two divs remains. This block
|
|
moves children of the second div in to the first div to get the correct
|
|
result:
|
|
<div>ad</div>
|
|
|
|
TODO(robbyw): Should we wrap the second div's contents in a span if they
|
|
have inline style?
|
|
*/
|
|
var rangeStart = goog.editor.style.getContainer(range.getStartNode());
|
|
var redundantContainer = goog.editor.node.getNextSibling(rangeStart);
|
|
if (rangeStart && redundantContainer) {
|
|
goog.dom.append(rangeStart, redundantContainer.childNodes);
|
|
goog.dom.removeNode(redundantContainer);
|
|
}
|
|
}
|
|
|
|
if (reselect) {
|
|
// The contents of the original range are gone, so restore the cursor
|
|
// position at the start of where the range once was.
|
|
range = goog.dom.Range.createCaret(nodeOffset.findTargetNode(baseNode),
|
|
rangeOffset);
|
|
range.select();
|
|
}
|
|
}
|
|
|
|
return range;
|
|
};
|
|
|
|
|
|
/**
|
|
* Checks whether the whole range is in a single block-level element.
|
|
* @param {goog.dom.AbstractRange} range The range to check.
|
|
* @return {boolean} Whether the whole range is in a single block-level element.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {
|
|
// Find the block element containing the start of the selection.
|
|
var startContainer = range.getStartNode();
|
|
if (goog.editor.style.isContainer(startContainer)) {
|
|
startContainer = startContainer.childNodes[range.getStartOffset()] ||
|
|
startContainer;
|
|
}
|
|
startContainer = goog.editor.style.getContainer(startContainer);
|
|
|
|
// Find the block element containing the end of the selection.
|
|
var endContainer = range.getEndNode();
|
|
if (goog.editor.style.isContainer(endContainer)) {
|
|
endContainer = endContainer.childNodes[range.getEndOffset()] ||
|
|
endContainer;
|
|
}
|
|
endContainer = goog.editor.style.getContainer(endContainer);
|
|
|
|
// Compare the two.
|
|
return startContainer == endContainer;
|
|
};
|
|
|
|
|
|
/**
|
|
* Checks whether the end of the range is not at the end of a block-level
|
|
* element.
|
|
* @param {goog.dom.AbstractRange} range The range to check.
|
|
* @return {boolean} Whether the end of the range is not at the end of a
|
|
* block-level element.
|
|
* @private
|
|
*/
|
|
goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {
|
|
var endContainer = range.getEndNode();
|
|
var endOffset = range.getEndOffset();
|
|
var node = endContainer;
|
|
if (goog.editor.style.isContainer(node)) {
|
|
var child = node.childNodes[endOffset];
|
|
// Child is null when end offset is >= length, which indicates the entire
|
|
// container is selected. Otherwise, we also know the entire container
|
|
// is selected if the selection ends at a new container.
|
|
if (!child ||
|
|
child.nodeType == goog.dom.NodeType.ELEMENT &&
|
|
goog.editor.style.isContainer(child)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var container = goog.editor.style.getContainer(node);
|
|
while (container != node) {
|
|
if (goog.editor.node.getNextSibling(node)) {
|
|
return true;
|
|
}
|
|
node = node.parentNode;
|
|
}
|
|
|
|
return endOffset != goog.editor.node.getLength(endContainer);
|
|
};
|