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 |