Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2016 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "core/editing/commands/InsertIncrementalTextCommand.h" | |
| 6 | |
| 7 #include "core/dom/Document.h" | |
| 8 #include "core/dom/Element.h" | |
| 9 #include "core/dom/Text.h" | |
| 10 #include "core/editing/EditingUtilities.h" | |
| 11 #include "core/editing/Editor.h" | |
| 12 #include "core/editing/PlainTextRange.h" | |
| 13 #include "core/editing/VisibleUnits.h" | |
| 14 #include "core/frame/LocalFrame.h" | |
| 15 #include "core/html/HTMLSpanElement.h" | |
| 16 | |
| 17 namespace blink { | |
| 18 | |
| 19 InsertIncrementalTextCommand::InsertIncrementalTextCommand( | |
| 20 Document& document, | |
| 21 const String& text, | |
| 22 bool selectInsertedText, | |
| 23 RebalanceType rebalanceType) | |
| 24 : CompositeEditCommand(document), | |
| 25 m_text(text), | |
| 26 m_selectInsertedText(selectInsertedText), | |
| 27 m_rebalanceType(rebalanceType) {} | |
| 28 | |
| 29 String InsertIncrementalTextCommand::textDataForInputEvent() const { | |
| 30 return m_text; | |
| 31 } | |
| 32 | |
| 33 Position InsertIncrementalTextCommand::positionInsideTextNode( | |
| 34 const Position& p, | |
| 35 EditingState* editingState) { | |
| 36 Position pos = p; | |
| 37 if (isTabHTMLSpanElementTextNode(pos.anchorNode())) { | |
| 38 Text* textNode = document().createEditingTextNode(""); | |
| 39 insertNodeAtTabSpanPosition(textNode, pos, editingState); | |
| 40 if (editingState->isAborted()) | |
| 41 return Position(); | |
| 42 return Position::firstPositionInNode(textNode); | |
| 43 } | |
| 44 | |
| 45 // Prepare for text input by looking at the specified position. | |
| 46 // It may be necessary to insert a text node to receive characters. | |
| 47 if (!pos.computeContainerNode()->isTextNode()) { | |
| 48 Text* textNode = document().createEditingTextNode(""); | |
| 49 insertNodeAt(textNode, pos, editingState); | |
| 50 if (editingState->isAborted()) | |
| 51 return Position(); | |
| 52 return Position::firstPositionInNode(textNode); | |
| 53 } | |
| 54 | |
| 55 return pos; | |
| 56 } | |
| 57 | |
| 58 void InsertIncrementalTextCommand::setEndingSelectionWithoutValidation( | |
| 59 const Position& startPosition, | |
| 60 const Position& endPosition) { | |
| 61 // We could have inserted a part of composed character sequence, | |
| 62 // so we are basically treating ending selection as a range to avoid | |
| 63 // validation. <http://bugs.webkit.org/show_bug.cgi?id=15781> | |
| 64 setEndingSelection(SelectionInDOMTree::Builder() | |
| 65 .collapse(startPosition) | |
| 66 .extend(endPosition) | |
| 67 .setIsDirectional(endingSelection().isDirectional()) | |
| 68 .build()); | |
| 69 } | |
| 70 | |
| 71 // This avoids the expense of a full fledged delete operation, and avoids a | |
| 72 // layout that typically results from text removal. | |
| 73 bool InsertIncrementalTextCommand::performTrivialReplace( | |
| 74 const String& text, | |
| 75 bool selectInsertedText) { | |
| 76 if (!endingSelection().isRange()) | |
| 77 return false; | |
| 78 | |
| 79 if (text.contains('\t') || text.contains(' ') || text.contains('\n')) | |
| 80 return false; | |
| 81 | |
| 82 Position start = endingSelection().start(); | |
| 83 Position endPosition = replaceSelectedTextInNode(text); | |
| 84 if (endPosition.isNull()) | |
| 85 return false; | |
| 86 | |
| 87 setEndingSelectionWithoutValidation(start, endPosition); | |
| 88 if (selectInsertedText) | |
| 89 return true; | |
| 90 setEndingSelection(SelectionInDOMTree::Builder() | |
| 91 .collapse(endingSelection().end()) | |
| 92 .setIsDirectional(endingSelection().isDirectional()) | |
| 93 .build()); | |
| 94 return true; | |
| 95 } | |
| 96 | |
| 97 bool InsertIncrementalTextCommand::performOverwrite(const String& text, | |
| 98 bool selectInsertedText) { | |
| 99 Position start = endingSelection().start(); | |
| 100 if (start.isNull() || !start.isOffsetInAnchor() || | |
| 101 !start.computeContainerNode()->isTextNode()) | |
| 102 return false; | |
| 103 Text* textNode = toText(start.computeContainerNode()); | |
| 104 if (!textNode) | |
| 105 return false; | |
| 106 | |
| 107 unsigned count = std::min(text.length(), | |
| 108 textNode->length() - start.offsetInContainerNode()); | |
| 109 if (!count) | |
| 110 return false; | |
| 111 | |
| 112 replaceTextInNode(textNode, start.offsetInContainerNode(), count, text); | |
| 113 | |
| 114 Position endPosition = | |
| 115 Position(textNode, start.offsetInContainerNode() + text.length()); | |
| 116 setEndingSelectionWithoutValidation(start, endPosition); | |
| 117 if (selectInsertedText || endingSelection().isNone()) | |
| 118 return true; | |
| 119 setEndingSelection(SelectionInDOMTree::Builder() | |
| 120 .collapse(endingSelection().end()) | |
| 121 .setIsDirectional(endingSelection().isDirectional()) | |
| 122 .build()); | |
| 123 return true; | |
| 124 } | |
| 125 | |
| 126 static size_t computeCommonPrefixLength(const String& str1, | |
| 127 const String& str2) { | |
| 128 const size_t maxCommonPrefixLength = std::min(str1.length(), str2.length()); | |
| 129 for (size_t index = 0; index < maxCommonPrefixLength; ++index) { | |
| 130 if (str1[index] != str2[index]) | |
| 131 return index; | |
| 132 } | |
| 133 return maxCommonPrefixLength; | |
| 134 } | |
| 135 | |
| 136 static size_t computeCommonSuffixLength(const String& str1, | |
| 137 const String& str2) { | |
| 138 const size_t length1 = str1.length(); | |
| 139 const size_t length2 = str2.length(); | |
| 140 const size_t maxCommonSuffixLength = std::min(length1, length2); | |
| 141 for (size_t index = 0; index < maxCommonSuffixLength; ++index) { | |
| 142 if (str1[length1 - index - 1] != str2[length2 - index - 1]) | |
| 143 return index; | |
| 144 } | |
| 145 return maxCommonSuffixLength; | |
| 146 } | |
| 147 | |
| 148 // If current position is at grapheme boundary, return 0; otherwise, return the | |
| 149 // distance to its nearest left grapheme boundary. | |
| 150 static size_t computeDistanceToLeftGraphemeBoundary(const Position& position) { | |
| 151 const Position& adjustedPosition = previousPositionOf( | |
| 152 nextPositionOf(position, PositionMoveType::GraphemeCluster), | |
| 153 PositionMoveType::GraphemeCluster); | |
| 154 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode()); | |
| 155 DCHECK_GE(position.computeOffsetInContainerNode(), | |
| 156 adjustedPosition.computeOffsetInContainerNode()); | |
| 157 return static_cast<size_t>(position.computeOffsetInContainerNode() - | |
| 158 adjustedPosition.computeOffsetInContainerNode()); | |
| 159 } | |
| 160 | |
| 161 static size_t computeCommonGraphemeClusterPrefixLength( | |
| 162 const String& oldText, | |
| 163 const String& newText, | |
| 164 const Element* rootEditableElement) { | |
| 165 const size_t commonPrefixLength = computeCommonPrefixLength(oldText, newText); | |
| 166 | |
| 167 // For grapheme cluster, we should adjust it for grapheme boundary. | |
| 168 const EphemeralRange& range = | |
| 169 PlainTextRange(0, commonPrefixLength).createRange(*rootEditableElement); | |
| 170 if (range.isNull()) | |
| 171 return 0; | |
| 172 const Position& position = range.endPosition(); | |
| 173 const size_t diff = computeDistanceToLeftGraphemeBoundary(position); | |
| 174 DCHECK_GE(commonPrefixLength, diff); | |
| 175 return commonPrefixLength - diff; | |
| 176 } | |
| 177 | |
| 178 // If current position is at grapheme boundary, return 0; otherwise, return the | |
| 179 // distance to its nearest right grapheme boundary. | |
| 180 static size_t computeDistanceToRightGraphemeBoundary(const Position& position) { | |
| 181 const Position& adjustedPosition = nextPositionOf( | |
| 182 previousPositionOf(position, PositionMoveType::GraphemeCluster), | |
| 183 PositionMoveType::GraphemeCluster); | |
| 184 DCHECK_EQ(position.anchorNode(), adjustedPosition.anchorNode()); | |
| 185 DCHECK_GE(adjustedPosition.computeOffsetInContainerNode(), | |
| 186 position.computeOffsetInContainerNode()); | |
| 187 return static_cast<size_t>(adjustedPosition.computeOffsetInContainerNode() - | |
| 188 position.computeOffsetInContainerNode()); | |
| 189 } | |
| 190 | |
| 191 static size_t computeCommonGraphemeClusterSuffixLength( | |
| 192 const String& oldText, | |
| 193 const String& newText, | |
| 194 const Element* rootEditableElement) { | |
| 195 const size_t commonSuffixLength = computeCommonSuffixLength(oldText, newText); | |
| 196 | |
| 197 // For grapheme cluster, we should adjust it for grapheme boundary. | |
| 198 const EphemeralRange& range = | |
| 199 PlainTextRange(0, oldText.length() - commonSuffixLength) | |
| 200 .createRange(*rootEditableElement); | |
| 201 if (range.isNull()) | |
| 202 return 0; | |
| 203 const Position& position = range.endPosition(); | |
| 204 const size_t diff = computeDistanceToRightGraphemeBoundary(position); | |
| 205 DCHECK_GE(commonSuffixLength, diff); | |
| 206 return commonSuffixLength - diff; | |
| 207 } | |
| 208 | |
| 209 static PlainTextRange getSelectionOffsets(LocalFrame* frame) { | |
| 210 EphemeralRange range = firstEphemeralRangeOf(frame->selection().selection()); | |
| 211 if (range.isNull()) | |
| 212 return PlainTextRange(); | |
| 213 ContainerNode* editable = | |
| 214 frame->selection().rootEditableElementOrTreeScopeRootNode(); | |
| 215 DCHECK(editable); | |
| 216 | |
| 217 return PlainTextRange::create(*editable, range); | |
| 218 } | |
| 219 | |
| 220 static const VisibleSelection createSelectionForIncrementalInsertion( | |
| 221 const size_t start, | |
| 222 const size_t end, | |
| 223 const bool isDirectional, | |
| 224 LocalFrame* frame) { | |
| 225 Element* element = frame->selection().selection().rootEditableElement(); | |
| 226 DCHECK(element); | |
| 227 | |
| 228 const EphemeralRange& startRange = | |
| 229 PlainTextRange(0, static_cast<int>(start)).createRange(*element); | |
| 230 DCHECK(startRange.isNotNull()); | |
| 231 const Position& startPosition = startRange.endPosition(); | |
| 232 | |
| 233 const EphemeralRange& endRange = | |
| 234 PlainTextRange(0, static_cast<int>(end)).createRange(*element); | |
| 235 DCHECK(endRange.isNotNull()); | |
| 236 const Position& endPosition = endRange.endPosition(); | |
| 237 | |
| 238 VisibleSelection selection = | |
| 239 createVisibleSelection(SelectionInDOMTree::Builder() | |
| 240 .setBaseAndExtent(startPosition, endPosition) | |
| 241 .build()); | |
| 242 selection.setIsDirectional(isDirectional); | |
| 243 | |
| 244 return selection; | |
| 245 } | |
| 246 | |
| 247 void InsertIncrementalTextCommand::setSelection(const size_t start, | |
| 248 const size_t end, | |
| 249 LocalFrame* frame) { | |
| 250 const VisibleSelection selection = createSelectionForIncrementalInsertion( | |
| 251 start, end, endingSelection().isDirectional(), frame); | |
| 252 setStartingSelection(selection); | |
| 253 setEndingSelectionWithoutValidation(selection.start(), selection.end()); | |
| 254 | |
| 255 document().frame()->selection().setSelection(selection); | |
| 256 } | |
| 257 | |
| 258 void InsertIncrementalTextCommand::doApply(EditingState* editingState) { | |
| 259 DCHECK_EQ(m_text.find('\n'), kNotFound); | |
| 260 | |
| 261 if (!endingSelection().isNonOrphanedCaretOrRange()) | |
| 262 return; | |
| 263 | |
| 264 LocalFrame* frame = document().frame(); | |
| 265 DCHECK(frame); | |
| 266 const Element* element = endingSelection().rootEditableElement(); | |
| 267 DCHECK(element); | |
| 268 | |
| 269 const String& newText = m_text; | |
| 270 const String oldText = frame->selectedText(); | |
| 271 | |
| 272 const size_t newTextLength = newText.length(); | |
| 273 const size_t commonPrefixLength = | |
| 274 computeCommonGraphemeClusterPrefixLength(oldText, newText, element); | |
| 275 | |
| 276 // We should ignore common prefix when finding common suffix. | |
| 277 const size_t commonSuffixLength = computeCommonGraphemeClusterSuffixLength( | |
| 278 oldText.right(oldText.length() - commonPrefixLength), | |
| 279 newText.right(newTextLength - commonPrefixLength), element); | |
| 280 | |
| 281 const String textToInsert = | |
| 282 newText.substring(commonPrefixLength, newTextLength - commonPrefixLength - | |
| 283 commonSuffixLength); | |
| 284 | |
| 285 const PlainTextRange selectionOffsets = getSelectionOffsets(frame); | |
| 286 const size_t selecitonStart = selectionOffsets.start(); | |
| 287 const size_t selectionEnd = selectionOffsets.end(); | |
| 288 const size_t insertionStart = selecitonStart + commonPrefixLength; | |
| 289 const size_t insertionEnd = selectionEnd - commonSuffixLength; | |
| 290 DCHECK_LE(insertionStart, insertionEnd); | |
| 291 | |
| 292 const VisibleSelection selectionForInsertion = | |
| 293 createSelectionForIncrementalInsertion(insertionStart, insertionEnd, | |
| 294 endingSelection().isDirectional(), | |
| 295 frame); | |
| 296 const bool changeSelection = selectionForInsertion != endingSelection(); | |
| 297 | |
| 298 setStartingSelection(selectionForInsertion); | |
| 299 setEndingSelectionWithoutValidation(selectionForInsertion.start(), | |
| 300 selectionForInsertion.end()); | |
| 301 | |
| 302 // Delete the current selection. | |
|
chongz
2016/12/02 20:34:44
I'm not sure if I fully understand the difficultie
yabinh
2016/12/05 08:09:39
Done.
| |
| 303 if (endingSelection().isRange()) { | |
| 304 if (performTrivialReplace(textToInsert, m_selectInsertedText)) { | |
| 305 if (changeSelection) | |
| 306 setSelection(selecitonStart, selecitonStart + newTextLength, frame); | |
| 307 return; | |
| 308 } | |
| 309 document().updateStyleAndLayoutIgnorePendingStylesheets(); | |
| 310 const bool endOfSelectionWasAtStartOfBlock = | |
| 311 isStartOfBlock(endingSelection().visibleEnd()); | |
| 312 deleteSelection(editingState, false, true, false, false); | |
| 313 if (editingState->isAborted()) | |
| 314 return; | |
| 315 | |
| 316 // deleteSelection eventually makes a new endingSelection out of a Position. | |
| 317 // If that Position doesn't have a layoutObject (e.g. it is on a <frameset> | |
| 318 // in the DOM), the VisibleSelection cannot be canonicalized to anything | |
| 319 // other than NoSelection. The rest of this function requires a real | |
| 320 // endingSelection, so bail out. | |
| 321 if (endingSelection().isNone()) | |
| 322 return; | |
| 323 if (endOfSelectionWasAtStartOfBlock) { | |
| 324 if (EditingStyle* typingStyle = frame->selection().typingStyle()) | |
| 325 typingStyle->removeBlockProperties(); | |
| 326 } | |
| 327 } else if (frame->editor().isOverwriteModeEnabled()) { | |
| 328 if (performOverwrite(textToInsert, m_selectInsertedText)) { | |
| 329 if (changeSelection) | |
| 330 setSelection(selecitonStart, selecitonStart + newTextLength, frame); | |
| 331 return; | |
| 332 } | |
| 333 } | |
| 334 | |
| 335 document().updateStyleAndLayoutIgnorePendingStylesheets(); | |
| 336 | |
| 337 Position startPosition(endingSelection().start()); | |
| 338 | |
| 339 Position placeholder; | |
| 340 // We want to remove preserved newlines and brs that will collapse (and thus | |
| 341 // become unnecessary) when content is inserted just before them. | |
| 342 // FIXME: We shouldn't really have to do this, but removing placeholders is a | |
| 343 // workaround for 9661. | |
| 344 // If the caret is just before a placeholder, downstream will normalize the | |
| 345 // caret to it. | |
| 346 Position downstream(mostForwardCaretPosition(startPosition)); | |
| 347 if (lineBreakExistsAtPosition(downstream)) { | |
| 348 // FIXME: This doesn't handle placeholders at the end of anonymous blocks. | |
| 349 VisiblePosition caret = createVisiblePosition(startPosition); | |
| 350 if (isEndOfBlock(caret) && isStartOfParagraph(caret)) | |
| 351 placeholder = downstream; | |
| 352 // Don't remove the placeholder yet, otherwise the block we're inserting | |
| 353 // into would collapse before we get a chance to insert into it. We check | |
| 354 // for a placeholder now, though, because doing so requires the creation of | |
| 355 // a VisiblePosition, and if we did that post-insertion it would force a | |
| 356 // layout. | |
| 357 } | |
| 358 | |
| 359 // Insert the character at the leftmost candidate. | |
| 360 startPosition = mostBackwardCaretPosition(startPosition); | |
| 361 | |
| 362 // It is possible for the node that contains startPosition to contain only | |
| 363 // unrendered whitespace, and so deleteInsignificantText could remove it. | |
| 364 // Save the position before the node in case that happens. | |
| 365 DCHECK(startPosition.computeContainerNode()) << startPosition; | |
| 366 Position positionBeforeStartNode( | |
| 367 Position::inParentBeforeNode(*startPosition.computeContainerNode())); | |
| 368 deleteInsignificantText(startPosition, | |
| 369 mostForwardCaretPosition(startPosition)); | |
| 370 if (!startPosition.isConnected()) | |
| 371 startPosition = positionBeforeStartNode; | |
| 372 if (!isVisuallyEquivalentCandidate(startPosition)) | |
| 373 startPosition = mostForwardCaretPosition(startPosition); | |
| 374 | |
| 375 startPosition = | |
| 376 positionAvoidingSpecialElementBoundary(startPosition, editingState); | |
| 377 if (editingState->isAborted()) | |
| 378 return; | |
| 379 | |
| 380 Position endPosition; | |
| 381 | |
| 382 if (textToInsert == "\t" && isRichlyEditablePosition(startPosition)) { | |
| 383 endPosition = insertTab(startPosition, editingState); | |
| 384 if (editingState->isAborted()) | |
| 385 return; | |
| 386 startPosition = | |
| 387 previousPositionOf(endPosition, PositionMoveType::GraphemeCluster); | |
| 388 if (placeholder.isNotNull()) | |
| 389 removePlaceholderAt(placeholder); | |
| 390 } else { | |
| 391 // Make sure the document is set up to receive textToInsert | |
| 392 startPosition = positionInsideTextNode(startPosition, editingState); | |
| 393 if (editingState->isAborted()) | |
| 394 return; | |
| 395 DCHECK(startPosition.isOffsetInAnchor()) << startPosition; | |
| 396 DCHECK(startPosition.computeContainerNode()) << startPosition; | |
| 397 DCHECK(startPosition.computeContainerNode()->isTextNode()) << startPosition; | |
| 398 if (placeholder.isNotNull()) | |
| 399 removePlaceholderAt(placeholder); | |
| 400 Text* textNode = toText(startPosition.computeContainerNode()); | |
| 401 const unsigned offset = startPosition.offsetInContainerNode(); | |
| 402 | |
| 403 insertTextIntoNode(textNode, offset, textToInsert); | |
| 404 endPosition = Position(textNode, offset + textToInsert.length()); | |
| 405 | |
| 406 if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) { | |
| 407 // The insertion may require adjusting adjacent whitespace, if it is | |
| 408 // present. | |
| 409 rebalanceWhitespaceAt(endPosition); | |
| 410 // Rebalancing on both sides isn't necessary if we've inserted only | |
| 411 // spaces. | |
| 412 if (!shouldRebalanceLeadingWhitespaceFor(textToInsert)) | |
| 413 rebalanceWhitespaceAt(startPosition); | |
| 414 } else { | |
| 415 DCHECK_EQ(m_rebalanceType, RebalanceAllWhitespaces); | |
| 416 if (canRebalance(startPosition) && canRebalance(endPosition)) { | |
| 417 rebalanceWhitespaceOnTextSubstring( | |
| 418 textNode, startPosition.offsetInContainerNode(), | |
| 419 endPosition.offsetInContainerNode()); | |
| 420 } | |
| 421 } | |
| 422 } | |
| 423 | |
| 424 setEndingSelectionWithoutValidation(startPosition, endPosition); | |
| 425 | |
| 426 // Handle the case where there is a typing style. | |
| 427 if (EditingStyle* typingStyle = frame->selection().typingStyle()) { | |
| 428 typingStyle->prepareToApplyAt(endPosition, | |
| 429 EditingStyle::PreserveWritingDirection); | |
| 430 if (!typingStyle->isEmpty()) { | |
| 431 applyStyle(typingStyle, editingState); | |
| 432 if (editingState->isAborted()) | |
| 433 return; | |
| 434 } | |
| 435 } | |
| 436 | |
| 437 if (changeSelection) | |
| 438 setSelection(selecitonStart, selecitonStart + newTextLength, frame); | |
| 439 | |
| 440 if (!m_selectInsertedText) { | |
| 441 SelectionInDOMTree::Builder builder; | |
| 442 builder.setAffinity(endingSelection().affinity()); | |
| 443 builder.setIsDirectional(endingSelection().isDirectional()); | |
| 444 if (endingSelection().end().isNotNull()) | |
| 445 builder.collapse(endingSelection().end()); | |
| 446 setEndingSelection(builder.build()); | |
| 447 } | |
| 448 } | |
| 449 | |
| 450 Position InsertIncrementalTextCommand::insertTab(const Position& pos, | |
| 451 EditingState* editingState) { | |
| 452 document().updateStyleAndLayoutIgnorePendingStylesheets(); | |
| 453 | |
| 454 Position insertPos = createVisiblePosition(pos).deepEquivalent(); | |
| 455 if (insertPos.isNull()) | |
| 456 return pos; | |
| 457 | |
| 458 Node* node = insertPos.computeContainerNode(); | |
| 459 unsigned offset = node->isTextNode() ? insertPos.offsetInContainerNode() : 0; | |
| 460 | |
| 461 // keep tabs coalesced in tab span | |
| 462 if (isTabHTMLSpanElementTextNode(node)) { | |
| 463 Text* textNode = toText(node); | |
| 464 insertTextIntoNode(textNode, offset, "\t"); | |
| 465 return Position(textNode, offset + 1); | |
| 466 } | |
| 467 | |
| 468 // create new tab span | |
| 469 HTMLSpanElement* spanElement = createTabSpanElement(document()); | |
| 470 | |
| 471 // place it | |
| 472 if (!node->isTextNode()) { | |
| 473 insertNodeAt(spanElement, insertPos, editingState); | |
| 474 } else { | |
| 475 Text* textNode = toText(node); | |
| 476 if (offset >= textNode->length()) { | |
| 477 insertNodeAfter(spanElement, textNode, editingState); | |
| 478 } else { | |
| 479 // split node to make room for the span | |
| 480 // NOTE: splitTextNode uses textNode for the | |
| 481 // second node in the split, so we need to | |
| 482 // insert the span before it. | |
| 483 if (offset > 0) | |
| 484 splitTextNode(textNode, offset); | |
| 485 insertNodeBefore(spanElement, textNode, editingState); | |
| 486 } | |
| 487 } | |
| 488 if (editingState->isAborted()) | |
| 489 return Position(); | |
| 490 | |
| 491 // return the position following the new tab | |
| 492 return Position::lastPositionInNode(spanElement); | |
| 493 } | |
| 494 | |
| 495 } // namespace blink | |
| OLD | NEW |