Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/plugins/enterhandler.js
2868 views
1
// Copyright 2008 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview Plugin to handle enter keys.
17
*
18
* @author [email protected] (Robby Walker)
19
*/
20
21
goog.provide('goog.editor.plugins.EnterHandler');
22
23
goog.require('goog.dom');
24
goog.require('goog.dom.NodeOffset');
25
goog.require('goog.dom.NodeType');
26
goog.require('goog.dom.Range');
27
goog.require('goog.dom.TagName');
28
goog.require('goog.editor.BrowserFeature');
29
goog.require('goog.editor.Plugin');
30
goog.require('goog.editor.node');
31
goog.require('goog.editor.plugins.Blockquote');
32
goog.require('goog.editor.range');
33
goog.require('goog.editor.style');
34
goog.require('goog.events.KeyCodes');
35
goog.require('goog.functions');
36
goog.require('goog.object');
37
goog.require('goog.string');
38
goog.require('goog.userAgent');
39
40
41
42
/**
43
* Plugin to handle enter keys. This does all the crazy to normalize (as much as
44
* is reasonable) what happens when you hit enter. This also handles the
45
* special casing of hitting enter in a blockquote.
46
*
47
* In IE, Webkit, and Opera, the resulting HTML uses one DIV tag per line. In
48
* Firefox, the resulting HTML uses BR tags at the end of each line.
49
*
50
* @constructor
51
* @extends {goog.editor.Plugin}
52
*/
53
goog.editor.plugins.EnterHandler = function() {
54
goog.editor.Plugin.call(this);
55
};
56
goog.inherits(goog.editor.plugins.EnterHandler, goog.editor.Plugin);
57
58
59
/**
60
* The type of block level tag to add on enter, for browsers that support
61
* specifying the default block-level tag. Can be overriden by subclasses; must
62
* be either DIV or P.
63
* @type {!goog.dom.TagName}
64
* @protected
65
*/
66
goog.editor.plugins.EnterHandler.prototype.tag = goog.dom.TagName.DIV;
67
68
69
/** @override */
70
goog.editor.plugins.EnterHandler.prototype.getTrogClassId = function() {
71
return 'EnterHandler';
72
};
73
74
75
/** @override */
76
goog.editor.plugins.EnterHandler.prototype.enable = function(fieldObject) {
77
goog.editor.plugins.EnterHandler.base(this, 'enable', fieldObject);
78
79
if (goog.editor.BrowserFeature.SUPPORTS_OPERA_DEFAULTBLOCK_COMMAND &&
80
(this.tag == goog.dom.TagName.P || this.tag == goog.dom.TagName.DIV)) {
81
var doc = this.getFieldDomHelper().getDocument();
82
doc.execCommand('opera-defaultBlock', false, this.tag);
83
}
84
};
85
86
87
/**
88
* If the contents are empty, return the 'default' html for the field.
89
* The 'default' contents depend on the enter handling mode, so it
90
* makes the most sense in this plugin.
91
* @param {string} html The html to prepare.
92
* @return {string} The original HTML, or default contents if that
93
* html is empty.
94
* @override
95
*/
96
goog.editor.plugins.EnterHandler.prototype.prepareContentsHtml = function(
97
html) {
98
if (!html || goog.string.isBreakingWhitespace(html)) {
99
return goog.editor.BrowserFeature.COLLAPSES_EMPTY_NODES ?
100
this.getNonCollapsingBlankHtml() :
101
'';
102
}
103
return html;
104
};
105
106
107
/**
108
* Gets HTML with no contents that won't collapse, for browsers that
109
* collapse the empty string.
110
* @return {string} Blank html.
111
* @protected
112
*/
113
goog.editor.plugins.EnterHandler.prototype.getNonCollapsingBlankHtml =
114
goog.functions.constant('<br>');
115
116
117
/**
118
* Internal backspace handler.
119
* @param {goog.events.Event} e The keypress event.
120
* @param {goog.dom.AbstractRange} range The closure range object.
121
* @protected
122
*/
123
goog.editor.plugins.EnterHandler.prototype.handleBackspaceInternal = function(
124
e, range) {
125
var field = this.getFieldObject().getElement();
126
var container = range && range.getStartNode();
127
128
if (field.firstChild == container && goog.editor.node.isEmpty(container)) {
129
e.preventDefault();
130
// TODO(user): I think we probably don't need to stopPropagation here
131
e.stopPropagation();
132
}
133
};
134
135
136
/**
137
* Fix paragraphs to be the correct type of node.
138
* @param {goog.events.Event} e The `<enter>` key event.
139
* @param {boolean} split Whether we already split up a blockquote by
140
* manually inserting elements.
141
* @protected
142
*/
143
goog.editor.plugins.EnterHandler.prototype.processParagraphTagsInternal =
144
function(e, split) {
145
// Force IE to turn the node we are leaving into a DIV. If we do turn
146
// it into a DIV, the node IE creates in response to ENTER will also be
147
// a DIV. If we don't, it will be a P. We handle that case
148
// in handleKeyUpIE_
149
if (goog.userAgent.IE || goog.userAgent.OPERA) {
150
this.ensureBlockIeOpera(goog.dom.TagName.DIV);
151
} else if (!split && goog.userAgent.WEBKIT) {
152
// WebKit duplicates a blockquote when the user hits enter. Let's cancel
153
// this and insert a BR instead, to make it more consistent with the other
154
// browsers.
155
var range = this.getFieldObject().getRange();
156
if (!range ||
157
!goog.editor.plugins.EnterHandler.isDirectlyInBlockquote(
158
range.getContainerElement())) {
159
return;
160
}
161
162
var dh = this.getFieldDomHelper();
163
var br = dh.createElement(goog.dom.TagName.BR);
164
range.insertNode(br, true);
165
166
// If the BR is at the end of a block element, Safari still thinks there is
167
// only one line instead of two, so we need to add another BR in that case.
168
if (goog.editor.node.isBlockTag(br.parentNode) &&
169
!goog.editor.node.skipEmptyTextNodes(br.nextSibling)) {
170
goog.dom.insertSiblingBefore(dh.createElement(goog.dom.TagName.BR), br);
171
}
172
173
goog.editor.range.placeCursorNextTo(br, false);
174
e.preventDefault();
175
}
176
};
177
178
179
/**
180
* Determines whether the lowest containing block node is a blockquote.
181
* @param {Node} n The node.
182
* @return {boolean} Whether the deepest block ancestor of n is a blockquote.
183
*/
184
goog.editor.plugins.EnterHandler.isDirectlyInBlockquote = function(n) {
185
for (var current = n; current; current = current.parentNode) {
186
if (goog.editor.node.isBlockTag(current)) {
187
return /** @type {!Element} */ (current).tagName ==
188
goog.dom.TagName.BLOCKQUOTE;
189
}
190
}
191
192
return false;
193
};
194
195
196
/**
197
* Internal delete key handler.
198
* @param {goog.events.Event} e The keypress event.
199
* @protected
200
*/
201
goog.editor.plugins.EnterHandler.prototype.handleDeleteGecko = function(e) {
202
this.deleteBrGecko(e);
203
};
204
205
206
/**
207
* Deletes the element at the cursor if it is a BR node, and if it does, calls
208
* e.preventDefault to stop the browser from deleting. Only necessary in Gecko
209
* as a workaround for mozilla bug 205350 where deleting a BR that is followed
210
* by a block element doesn't work (the BR gets immediately replaced). We also
211
* need to account for an ill-formed cursor which occurs from us trying to
212
* stop the browser from deleting.
213
*
214
* @param {goog.events.Event} e The DELETE keypress event.
215
* @protected
216
*/
217
goog.editor.plugins.EnterHandler.prototype.deleteBrGecko = function(e) {
218
var range = this.getFieldObject().getRange();
219
if (range.isCollapsed()) {
220
var container = range.getEndNode();
221
if (container.nodeType == goog.dom.NodeType.ELEMENT) {
222
var nextNode = container.childNodes[range.getEndOffset()];
223
if (nextNode && nextNode.tagName == goog.dom.TagName.BR) {
224
// We want to retrieve the first non-whitespace previous sibling
225
// as we could have added an empty text node below and want to
226
// properly handle deleting a sequence of BR's.
227
var previousSibling = goog.editor.node.getPreviousSibling(nextNode);
228
var nextSibling = nextNode.nextSibling;
229
230
container.removeChild(nextNode);
231
e.preventDefault();
232
233
// When we delete a BR followed by a block level element, the cursor
234
// has a line-height which spans the height of the block level element.
235
// e.g. If we delete a BR followed by a UL, the resulting HTML will
236
// appear to the end user like:-
237
//
238
// | * one
239
// | * two
240
// | * three
241
//
242
// There are a couple of cases that we have to account for in order to
243
// properly conform to what the user expects when DELETE is pressed.
244
//
245
// 1. If the BR has a previous sibling and the previous sibling is
246
// not a block level element or a BR, we place the cursor at the
247
// end of that.
248
// 2. If the BR doesn't have a previous sibling or the previous sibling
249
// is a block level element or a BR, we place the cursor at the
250
// beginning of the leftmost leaf of its next sibling.
251
if (nextSibling && goog.editor.node.isBlockTag(nextSibling)) {
252
if (previousSibling &&
253
!(previousSibling.tagName == goog.dom.TagName.BR ||
254
goog.editor.node.isBlockTag(previousSibling))) {
255
goog.dom.Range
256
.createCaret(
257
previousSibling,
258
goog.editor.node.getLength(previousSibling))
259
.select();
260
} else {
261
var leftMostLeaf = goog.editor.node.getLeftMostLeaf(nextSibling);
262
goog.dom.Range.createCaret(leftMostLeaf, 0).select();
263
}
264
}
265
}
266
}
267
}
268
};
269
270
271
/** @override */
272
goog.editor.plugins.EnterHandler.prototype.handleKeyPress = function(e) {
273
// If a dialog doesn't have selectable field, Gecko grabs the event and
274
// performs actions in editor window. This solves that problem and allows
275
// the event to be passed on to proper handlers.
276
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
277
return false;
278
}
279
280
// Firefox will allow the first node in an iframe to be deleted
281
// on a backspace. Disallow it if the node is empty.
282
if (e.keyCode == goog.events.KeyCodes.BACKSPACE) {
283
this.handleBackspaceInternal(e, this.getFieldObject().getRange());
284
285
} else if (e.keyCode == goog.events.KeyCodes.ENTER) {
286
if (goog.userAgent.GECKO) {
287
if (!e.shiftKey) {
288
// Behave similarly to IE's content editable return carriage:
289
// If the shift key is down or specified by the application, insert a
290
// BR, otherwise split paragraphs
291
this.handleEnterGecko_(e);
292
}
293
} else {
294
// In Gecko-based browsers, this is handled in the handleEnterGecko_
295
// method.
296
this.getFieldObject().dispatchBeforeChange();
297
var cursorPosition = this.deleteCursorSelection_();
298
299
var split = !!this.getFieldObject().execCommand(
300
goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
301
if (split) {
302
// TODO(user): I think we probably don't need to stopPropagation here
303
e.preventDefault();
304
e.stopPropagation();
305
}
306
307
this.releasePositionObject_(cursorPosition);
308
309
if (goog.userAgent.WEBKIT) {
310
this.handleEnterWebkitInternal(e);
311
}
312
313
this.processParagraphTagsInternal(e, split);
314
this.getFieldObject().dispatchChange();
315
}
316
317
} else if (goog.userAgent.GECKO && e.keyCode == goog.events.KeyCodes.DELETE) {
318
this.handleDeleteGecko(e);
319
}
320
321
return false;
322
};
323
324
325
/** @override */
326
goog.editor.plugins.EnterHandler.prototype.handleKeyUp = function(e) {
327
// If a dialog doesn't have selectable field, Gecko grabs the event and
328
// performs actions in editor window. This solves that problem and allows
329
// the event to be passed on to proper handlers.
330
if (goog.userAgent.GECKO && this.getFieldObject().inModalMode()) {
331
return false;
332
}
333
this.handleKeyUpInternal(e);
334
return false;
335
};
336
337
338
/**
339
* Internal handler for keyup events.
340
* @param {goog.events.Event} e The key event.
341
* @protected
342
*/
343
goog.editor.plugins.EnterHandler.prototype.handleKeyUpInternal = function(e) {
344
if ((goog.userAgent.IE || goog.userAgent.OPERA) &&
345
e.keyCode == goog.events.KeyCodes.ENTER) {
346
this.ensureBlockIeOpera(goog.dom.TagName.DIV, true);
347
}
348
};
349
350
351
/**
352
* Handles an enter keypress event on fields in Gecko.
353
* @param {goog.events.BrowserEvent} e The key event.
354
* @private
355
*/
356
goog.editor.plugins.EnterHandler.prototype.handleEnterGecko_ = function(e) {
357
// Retrieve whether the selection is collapsed before we delete it.
358
var range = this.getFieldObject().getRange();
359
var wasCollapsed = !range || range.isCollapsed();
360
var cursorPosition = this.deleteCursorSelection_();
361
362
var handled = this.getFieldObject().execCommand(
363
goog.editor.plugins.Blockquote.SPLIT_COMMAND, cursorPosition);
364
if (handled) {
365
// TODO(user): I think we probably don't need to stopPropagation here
366
e.preventDefault();
367
e.stopPropagation();
368
}
369
370
this.releasePositionObject_(cursorPosition);
371
if (!handled) {
372
this.handleEnterAtCursorGeckoInternal(e, wasCollapsed, range);
373
}
374
};
375
376
377
/**
378
* Handle an enter key press in WebKit.
379
* @param {goog.events.BrowserEvent} e The key press event.
380
* @protected
381
*/
382
goog.editor.plugins.EnterHandler.prototype.handleEnterWebkitInternal =
383
goog.nullFunction;
384
385
386
/**
387
* Handle an enter key press on collapsed selection. handleEnterGecko_ ensures
388
* the selection is collapsed by deleting its contents if it is not. The
389
* default implementation does nothing.
390
* @param {goog.events.BrowserEvent} e The key press event.
391
* @param {boolean} wasCollapsed Whether the selection was collapsed before
392
* the key press. If it was not, code before this function has already
393
* cleared the contents of the selection.
394
* @param {goog.dom.AbstractRange} range Object representing the selection.
395
* @protected
396
*/
397
goog.editor.plugins.EnterHandler.prototype.handleEnterAtCursorGeckoInternal =
398
goog.nullFunction;
399
400
401
/**
402
* Names of all the nodes that we don't want to turn into block nodes in IE when
403
* the user hits enter.
404
* @type {Object}
405
* @private
406
*/
407
goog.editor.plugins.EnterHandler.DO_NOT_ENSURE_BLOCK_NODES_ =
408
goog.object.createSet(
409
goog.dom.TagName.LI, goog.dom.TagName.DIV, goog.dom.TagName.H1,
410
goog.dom.TagName.H2, goog.dom.TagName.H3, goog.dom.TagName.H4,
411
goog.dom.TagName.H5, goog.dom.TagName.H6);
412
413
414
/**
415
* Whether this is a node that contains a single BR tag and non-nbsp
416
* whitespace.
417
* @param {Node} node Node to check.
418
* @return {boolean} Whether this is an element that only contains a BR.
419
* @protected
420
*/
421
goog.editor.plugins.EnterHandler.isBrElem = function(node) {
422
return goog.editor.node.isEmpty(node) &&
423
goog.dom.getElementsByTagName(
424
goog.dom.TagName.BR, /** @type {!Element} */ (node)).length == 1;
425
};
426
427
428
/**
429
* Ensures all text in IE and Opera to be in the given tag in order to control
430
* Enter spacing. Call this when Enter is pressed if desired.
431
*
432
* We want to make sure the user is always inside of a block (or other nodes
433
* listed in goog.editor.plugins.EnterHandler.IGNORE_ENSURE_BLOCK_NODES_). We
434
* listen to keypress to force nodes that the user is leaving to turn into
435
* blocks, but we also need to listen to keyup to force nodes that the user is
436
* entering to turn into blocks.
437
* Example: html is: `<h2>foo[cursor]</h2>`, and the user hits enter. We
438
* don't want to format the h2, but we do want to format the P that is
439
* created on enter. The P node is not available until keyup.
440
* @param {!goog.dom.TagName} tag The tag name to convert to.
441
* @param {boolean=} opt_keyUp Whether the function is being called on key up.
442
* When called on key up, the cursor is in the newly created node, so the
443
* semantics for when to change it to a block are different. Specifically,
444
* if the resulting node contains only a BR, it is converted to `<tag>`.
445
* @protected
446
*/
447
goog.editor.plugins.EnterHandler.prototype.ensureBlockIeOpera = function(
448
tag, opt_keyUp) {
449
var range = this.getFieldObject().getRange();
450
var container = range.getContainer();
451
var field = this.getFieldObject().getElement();
452
453
var paragraph;
454
while (container && container != field) {
455
// We don't need to ensure a block if we are already in the same block, or
456
// in another block level node that we don't want to change the format of
457
// (unless we're handling keyUp and that block node just contains a BR).
458
var nodeName = container.nodeName;
459
// Due to @bug 2455389, the call to isBrElem needs to be inlined in the if
460
// instead of done before and saved in a variable, so that it can be
461
// short-circuited and avoid a weird IE edge case.
462
if (nodeName == tag ||
463
(goog.editor.plugins.EnterHandler
464
.DO_NOT_ENSURE_BLOCK_NODES_[nodeName] &&
465
!(opt_keyUp &&
466
goog.editor.plugins.EnterHandler.isBrElem(container)))) {
467
// Opera can create a <p> inside of a <div> in some situations,
468
// such as when breaking out of a list that is contained in a <div>.
469
if (goog.userAgent.OPERA && paragraph) {
470
if (nodeName == tag && paragraph == container.lastChild &&
471
goog.editor.node.isEmpty(paragraph)) {
472
goog.dom.insertSiblingAfter(paragraph, container);
473
goog.dom.Range.createFromNodeContents(paragraph).select();
474
}
475
break;
476
}
477
return;
478
}
479
if (goog.userAgent.OPERA && opt_keyUp && nodeName == goog.dom.TagName.P &&
480
nodeName != tag) {
481
paragraph = container;
482
}
483
484
container = container.parentNode;
485
}
486
487
488
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher(9)) {
489
// IE (before IE9) has a bug where if the cursor is directly before a block
490
// node (e.g., the content is "foo[cursor]<blockquote>bar</blockquote>"),
491
// the FormatBlock command actually formats the "bar" instead of the "foo".
492
// This is just wrong. To work-around this, we want to move the
493
// selection back one character, and then restore it to its prior position.
494
// NOTE: We use the following "range math" to detect this situation because
495
// using Closure ranges here triggers a bug in IE that causes a crash.
496
// parent2 != parent3 ensures moving the cursor forward one character
497
// crosses at least 1 element boundary, and therefore tests if the cursor is
498
// at such a boundary. The second check, parent3 != range.parentElement()
499
// weeds out some cases where the elements are siblings instead of cousins.
500
var needsHelp = false;
501
range = range.getBrowserRangeObject();
502
var range2 = range.duplicate();
503
range2.moveEnd('character', 1);
504
// In whitebox mode, when the cursor is at the end of the field, trying to
505
// move the end of the range will do nothing, and hence the range's text
506
// will be empty. In this case, the cursor clearly isn't sitting just
507
// before a block node, since it isn't before anything.
508
if (range2.text.length) {
509
var parent2 = range2.parentElement();
510
511
var range3 = range2.duplicate();
512
range3.collapse(false);
513
var parent3 = range3.parentElement();
514
515
if ((needsHelp =
516
parent2 != parent3 && parent3 != range.parentElement())) {
517
range.move('character', -1);
518
range.select();
519
}
520
}
521
}
522
523
this.getFieldObject().getEditableDomHelper().getDocument().execCommand(
524
'FormatBlock', false, '<' + tag + '>');
525
526
if (needsHelp) {
527
range.move('character', 1);
528
range.select();
529
}
530
};
531
532
533
/**
534
* Deletes the content at the current cursor position.
535
* @return {!Node|!Object} Something representing the current cursor position.
536
* See deleteCursorSelectionIE_ and deleteCursorSelectionW3C_ for details.
537
* Should be passed to releasePositionObject_ when no longer in use.
538
* @private
539
*/
540
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelection_ = function() {
541
return goog.editor.BrowserFeature.HAS_W3C_RANGES ?
542
this.deleteCursorSelectionW3C_() :
543
this.deleteCursorSelectionIE_();
544
};
545
546
547
/**
548
* Releases the object returned by deleteCursorSelection_.
549
* @param {Node|Object} position The object returned by deleteCursorSelection_.
550
* @private
551
*/
552
goog.editor.plugins.EnterHandler.prototype.releasePositionObject_ = function(
553
position) {
554
if (!goog.editor.BrowserFeature.HAS_W3C_RANGES) {
555
(/** @type {Node} */ (position)).removeNode(true);
556
}
557
};
558
559
560
/**
561
* Delete the selection at the current cursor position, then returns a temporary
562
* node at the current position.
563
* @return {!Node} A temporary node marking the current cursor position. This
564
* node should eventually be removed from the DOM.
565
* @private
566
*/
567
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionIE_ =
568
function() {
569
var doc = this.getFieldDomHelper().getDocument();
570
var range = doc.selection.createRange();
571
572
var id = goog.string.createUniqueString();
573
range.pasteHTML('<span id="' + id + '"></span>');
574
var splitNode = doc.getElementById(id);
575
splitNode.id = '';
576
return splitNode;
577
};
578
579
580
/**
581
* Delete the selection at the current cursor position, then returns the node
582
* at the current position.
583
* @return {!goog.editor.range.Point} The current cursor position. Note that
584
* unlike simulateEnterIE_, this should not be removed from the DOM.
585
* @private
586
*/
587
goog.editor.plugins.EnterHandler.prototype.deleteCursorSelectionW3C_ =
588
function() {
589
var range = this.getFieldObject().getRange();
590
591
// Delete the current selection if it's is non-collapsed.
592
// Although this is redundant in FF, it's necessary for Safari
593
if (range && !range.isCollapsed()) {
594
var shouldDelete = true;
595
// Opera selects the <br> in an empty block if there is no text node
596
// preceding it. To preserve inline formatting when pressing [enter] inside
597
// an empty block, don't delete the selection if it only selects a <br> at
598
// the end of the block.
599
// TODO(user): Move this into goog.dom.Range. It should detect this state
600
// when creating a range from the window selection and fix it in the created
601
// range.
602
if (goog.userAgent.OPERA) {
603
var startNode = range.getStartNode();
604
var startOffset = range.getStartOffset();
605
if (startNode == range.getEndNode() &&
606
// This weeds out cases where startNode is a text node.
607
startNode.lastChild &&
608
/** @type {!Element} */ (startNode.lastChild).tagName ==
609
goog.dom.TagName.BR &&
610
// If this check is true, then endOffset is implied to be
611
// startOffset + 1, because the selection is not collapsed and
612
// it starts and ends within the same element.
613
startOffset == startNode.childNodes.length - 1) {
614
shouldDelete = false;
615
}
616
}
617
if (shouldDelete) {
618
goog.editor.plugins.EnterHandler.deleteW3cRange_(range);
619
}
620
}
621
622
return goog.editor.range.getDeepEndPoint(range, true);
623
};
624
625
626
/**
627
* Deletes the contents of the selection from the DOM.
628
* @param {goog.dom.AbstractRange} range The range to remove contents from.
629
* @return {goog.dom.AbstractRange} The resulting range. Used for testing.
630
* @private
631
*/
632
goog.editor.plugins.EnterHandler.deleteW3cRange_ = function(range) {
633
if (range && !range.isCollapsed()) {
634
var reselect = true;
635
var baseNode = range.getContainerElement();
636
var nodeOffset = new goog.dom.NodeOffset(range.getStartNode(), baseNode);
637
var rangeOffset = range.getStartOffset();
638
639
// Whether the selection crosses no container boundaries.
640
var isInOneContainer =
641
goog.editor.plugins.EnterHandler.isInOneContainerW3c_(range);
642
643
// Whether the selection ends in a container it doesn't fully select.
644
var isPartialEnd = !isInOneContainer &&
645
goog.editor.plugins.EnterHandler.isPartialEndW3c_(range);
646
647
// Remove The range contents, and ensure the correct content stays selected.
648
range.removeContents();
649
var node = nodeOffset.findTargetNode(baseNode);
650
if (node) {
651
range = goog.dom.Range.createCaret(node, rangeOffset);
652
} else {
653
// This occurs when the node that would have been referenced has now been
654
// deleted and there are no other nodes in the baseNode. Thus need to
655
// set the caret to the end of the base node.
656
range = goog.dom.Range.createCaret(baseNode, baseNode.childNodes.length);
657
reselect = false;
658
}
659
range.select();
660
661
// If we just deleted everything from the container, add an nbsp
662
// to the container, and leave the cursor inside of it
663
if (isInOneContainer) {
664
var container = goog.editor.style.getContainer(range.getStartNode());
665
if (goog.editor.node.isEmpty(container, true)) {
666
var html = '&nbsp;';
667
if (goog.userAgent.OPERA && container.tagName == goog.dom.TagName.LI) {
668
// Don't break Opera's native break-out-of-lists behavior.
669
html = '<br>';
670
}
671
goog.editor.node.replaceInnerHtml(container, html);
672
goog.editor.range.selectNodeStart(container.firstChild);
673
reselect = false;
674
}
675
}
676
677
if (isPartialEnd) {
678
/*
679
This code handles the following, where | is the cursor:
680
<div>a|b</div><div>c|d</div>
681
After removeContents, the remaining HTML is
682
<div>a</div><div>d</div>
683
which means the line break between the two divs remains. This block
684
moves children of the second div in to the first div to get the correct
685
result:
686
<div>ad</div>
687
688
TODO(robbyw): Should we wrap the second div's contents in a span if they
689
have inline style?
690
*/
691
var rangeStart = goog.editor.style.getContainer(range.getStartNode());
692
var redundantContainer = goog.editor.node.getNextSibling(rangeStart);
693
if (rangeStart && redundantContainer) {
694
goog.dom.append(rangeStart, redundantContainer.childNodes);
695
goog.dom.removeNode(redundantContainer);
696
}
697
}
698
699
if (reselect) {
700
// The contents of the original range are gone, so restore the cursor
701
// position at the start of where the range once was.
702
range = goog.dom.Range.createCaret(
703
nodeOffset.findTargetNode(baseNode), rangeOffset);
704
range.select();
705
}
706
}
707
708
return range;
709
};
710
711
712
/**
713
* Checks whether the whole range is in a single block-level element.
714
* @param {goog.dom.AbstractRange} range The range to check.
715
* @return {boolean} Whether the whole range is in a single block-level element.
716
* @private
717
*/
718
goog.editor.plugins.EnterHandler.isInOneContainerW3c_ = function(range) {
719
// Find the block element containing the start of the selection.
720
var startContainer = range.getStartNode();
721
if (goog.editor.style.isContainer(startContainer)) {
722
startContainer =
723
startContainer.childNodes[range.getStartOffset()] || startContainer;
724
}
725
startContainer = goog.editor.style.getContainer(startContainer);
726
727
// Find the block element containing the end of the selection.
728
var endContainer = range.getEndNode();
729
if (goog.editor.style.isContainer(endContainer)) {
730
endContainer =
731
endContainer.childNodes[range.getEndOffset()] || endContainer;
732
}
733
endContainer = goog.editor.style.getContainer(endContainer);
734
735
// Compare the two.
736
return startContainer == endContainer;
737
};
738
739
740
/**
741
* Checks whether the end of the range is not at the end of a block-level
742
* element.
743
* @param {goog.dom.AbstractRange} range The range to check.
744
* @return {boolean} Whether the end of the range is not at the end of a
745
* block-level element.
746
* @private
747
*/
748
goog.editor.plugins.EnterHandler.isPartialEndW3c_ = function(range) {
749
var endContainer = range.getEndNode();
750
var endOffset = range.getEndOffset();
751
var node = endContainer;
752
if (goog.editor.style.isContainer(node)) {
753
var child = node.childNodes[endOffset];
754
// Child is null when end offset is >= length, which indicates the entire
755
// container is selected. Otherwise, we also know the entire container
756
// is selected if the selection ends at a new container.
757
if (!child ||
758
child.nodeType == goog.dom.NodeType.ELEMENT &&
759
goog.editor.style.isContainer(child)) {
760
return false;
761
}
762
}
763
764
var container = goog.editor.style.getContainer(node);
765
while (container != node) {
766
if (goog.editor.node.getNextSibling(node)) {
767
return true;
768
}
769
node = node.parentNode;
770
}
771
772
return endOffset != goog.editor.node.getLength(endContainer);
773
};
774
775