1 | /* |
2 | * Copyright (C) 2005 Apple Inc. All rights reserved. |
3 | * |
4 | * Redistribution and use in source and binary forms, with or without |
5 | * modification, are permitted provided that the following conditions |
6 | * are met: |
7 | * 1. Redistributions of source code must retain the above copyright |
8 | * notice, this list of conditions and the following disclaimer. |
9 | * 2. Redistributions in binary form must reproduce the above copyright |
10 | * notice, this list of conditions and the following disclaimer in the |
11 | * documentation and/or other materials provided with the distribution. |
12 | * |
13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
14 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
15 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
17 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
18 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
19 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
20 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
21 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
22 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
23 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
24 | */ |
25 | |
26 | #include "config.h" |
27 | #include "InsertTextCommand.h" |
28 | |
29 | #include "Document.h" |
30 | #include "Editing.h" |
31 | #include "Editor.h" |
32 | #include "Frame.h" |
33 | #include "HTMLElement.h" |
34 | #include "HTMLInterchange.h" |
35 | #include "Text.h" |
36 | #include "VisibleUnits.h" |
37 | |
38 | namespace WebCore { |
39 | |
40 | InsertTextCommand::InsertTextCommand(Document& document, const String& text, bool selectInsertedText, RebalanceType rebalanceType, EditAction editingAction) |
41 | : CompositeEditCommand(document, editingAction) |
42 | , m_text(text) |
43 | , m_selectInsertedText(selectInsertedText) |
44 | , m_rebalanceType(rebalanceType) |
45 | { |
46 | } |
47 | |
48 | InsertTextCommand::InsertTextCommand(Document& document, const String& text, Ref<TextInsertionMarkerSupplier>&& markerSupplier, EditAction editingAction) |
49 | : CompositeEditCommand(document, editingAction) |
50 | , m_text(text) |
51 | , m_selectInsertedText(false) |
52 | , m_rebalanceType(RebalanceLeadingAndTrailingWhitespaces) |
53 | , m_markerSupplier(WTFMove(markerSupplier)) |
54 | { |
55 | } |
56 | |
57 | Position InsertTextCommand::positionInsideTextNode(const Position& p) |
58 | { |
59 | Position pos = p; |
60 | if (isTabSpanTextNode(pos.anchorNode())) { |
61 | auto textNode = document().createEditingTextNode(emptyString()); |
62 | auto* textNodePtr = textNode.ptr(); |
63 | insertNodeAtTabSpanPosition(WTFMove(textNode), pos); |
64 | return firstPositionInNode(textNodePtr); |
65 | } |
66 | |
67 | // Prepare for text input by looking at the specified position. |
68 | // It may be necessary to insert a text node to receive characters. |
69 | if (!pos.containerNode()->isTextNode()) { |
70 | auto textNode = document().createEditingTextNode(emptyString()); |
71 | auto* textNodePtr = textNode.ptr(); |
72 | insertNodeAt(WTFMove(textNode), pos); |
73 | return firstPositionInNode(textNodePtr); |
74 | } |
75 | |
76 | return pos; |
77 | } |
78 | |
79 | void InsertTextCommand::setEndingSelectionWithoutValidation(const Position& startPosition, const Position& endPosition) |
80 | { |
81 | // We could have inserted a part of composed character sequence, |
82 | // so we are basically treating ending selection as a range to avoid validation. |
83 | // <http://bugs.webkit.org/show_bug.cgi?id=15781> |
84 | VisibleSelection forcedEndingSelection; |
85 | forcedEndingSelection.setWithoutValidation(startPosition, endPosition); |
86 | forcedEndingSelection.setIsDirectional(endingSelection().isDirectional()); |
87 | setEndingSelection(forcedEndingSelection); |
88 | } |
89 | |
90 | // This avoids the expense of a full fledged delete operation, and avoids a layout that typically results |
91 | // from text removal. |
92 | bool InsertTextCommand::performTrivialReplace(const String& text, bool selectInsertedText) |
93 | { |
94 | if (!endingSelection().isRange()) |
95 | return false; |
96 | |
97 | if (text.contains('\t') || text.contains(' ') || text.contains('\n')) |
98 | return false; |
99 | |
100 | Position start = endingSelection().start(); |
101 | Position endPosition = replaceSelectedTextInNode(text); |
102 | if (endPosition.isNull()) |
103 | return false; |
104 | |
105 | setEndingSelectionWithoutValidation(start, endPosition); |
106 | if (!selectInsertedText) |
107 | setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional())); |
108 | |
109 | return true; |
110 | } |
111 | |
112 | bool InsertTextCommand::performOverwrite(const String& text, bool selectInsertedText) |
113 | { |
114 | Position start = endingSelection().start(); |
115 | RefPtr<Text> textNode = start.containerText(); |
116 | if (!textNode) |
117 | return false; |
118 | |
119 | unsigned count = std::min(text.length(), textNode->length() - start.offsetInContainerNode()); |
120 | if (!count) |
121 | return false; |
122 | |
123 | replaceTextInNode(*textNode, start.offsetInContainerNode(), count, text); |
124 | |
125 | Position endPosition = Position(textNode.get(), start.offsetInContainerNode() + text.length()); |
126 | setEndingSelectionWithoutValidation(start, endPosition); |
127 | if (!selectInsertedText) |
128 | setEndingSelection(VisibleSelection(endingSelection().visibleEnd(), endingSelection().isDirectional())); |
129 | |
130 | return true; |
131 | } |
132 | |
133 | void InsertTextCommand::doApply() |
134 | { |
135 | ASSERT(m_text.find('\n') == notFound); |
136 | |
137 | if (endingSelection().isNoneOrOrphaned()) |
138 | return; |
139 | |
140 | // Delete the current selection. |
141 | // FIXME: This delete operation blows away the typing style. |
142 | if (endingSelection().isRange()) { |
143 | if (performTrivialReplace(m_text, m_selectInsertedText)) |
144 | return; |
145 | deleteSelection(false, true, true, false, false); |
146 | // deleteSelection eventually makes a new endingSelection out of a Position. If that Position doesn't have |
147 | // a renderer (e.g. it is on a <frameset> in the DOM), the VisibleSelection cannot be canonicalized to |
148 | // anything other than NoSelection. The rest of this function requires a real endingSelection, so bail out. |
149 | if (endingSelection().isNone()) |
150 | return; |
151 | } else if (frame().editor().isOverwriteModeEnabled()) { |
152 | if (performOverwrite(m_text, m_selectInsertedText)) |
153 | return; |
154 | } |
155 | |
156 | Position startPosition(endingSelection().start()); |
157 | |
158 | Position placeholder; |
159 | // We want to remove preserved newlines and brs that will collapse (and thus become unnecessary) when content |
160 | // is inserted just before them. |
161 | // FIXME: We shouldn't really have to do this, but removing placeholders is a workaround for 9661. |
162 | // If the caret is just before a placeholder, downstream will normalize the caret to it. |
163 | Position downstream(startPosition.downstream()); |
164 | if (lineBreakExistsAtPosition(downstream)) { |
165 | // FIXME: This doesn't handle placeholders at the end of anonymous blocks. |
166 | VisiblePosition caret(startPosition); |
167 | if (isEndOfBlock(caret) && isStartOfParagraph(caret)) |
168 | placeholder = downstream; |
169 | // Don't remove the placeholder yet, otherwise the block we're inserting into would collapse before |
170 | // we get a chance to insert into it. We check for a placeholder now, though, because doing so requires |
171 | // the creation of a VisiblePosition, and if we did that post-insertion it would force a layout. |
172 | } |
173 | |
174 | // Insert the character at the leftmost candidate. |
175 | startPosition = startPosition.upstream(); |
176 | |
177 | // It is possible for the node that contains startPosition to contain only unrendered whitespace, |
178 | // and so deleteInsignificantText could remove it. Save the position before the node in case that happens. |
179 | Position positionBeforeStartNode(positionInParentBeforeNode(startPosition.containerNode())); |
180 | deleteInsignificantText(startPosition.upstream(), startPosition.downstream()); |
181 | if (!startPosition.anchorNode()->isConnected()) |
182 | startPosition = positionBeforeStartNode; |
183 | if (!startPosition.isCandidate()) |
184 | startPosition = startPosition.downstream(); |
185 | |
186 | startPosition = positionAvoidingSpecialElementBoundary(startPosition); |
187 | |
188 | Position endPosition; |
189 | |
190 | if (m_text == "\t" ) { |
191 | endPosition = insertTab(startPosition); |
192 | startPosition = endPosition.previous(); |
193 | if (placeholder.isNotNull()) |
194 | removePlaceholderAt(placeholder); |
195 | } else { |
196 | // Make sure the document is set up to receive m_text |
197 | startPosition = positionInsideTextNode(startPosition); |
198 | ASSERT(startPosition.anchorType() == Position::PositionIsOffsetInAnchor); |
199 | ASSERT(startPosition.containerNode()); |
200 | ASSERT(startPosition.containerNode()->isTextNode()); |
201 | if (placeholder.isNotNull()) |
202 | removePlaceholderAt(placeholder); |
203 | RefPtr<Text> textNode = startPosition.containerText(); |
204 | const unsigned offset = startPosition.offsetInContainerNode(); |
205 | |
206 | insertTextIntoNode(*textNode, offset, m_text); |
207 | endPosition = Position(textNode.get(), offset + m_text.length()); |
208 | if (m_markerSupplier) |
209 | m_markerSupplier->addMarkersToTextNode(*textNode, offset, m_text); |
210 | |
211 | if (m_rebalanceType == RebalanceLeadingAndTrailingWhitespaces) { |
212 | // The insertion may require adjusting adjacent whitespace, if it is present. |
213 | rebalanceWhitespaceAt(endPosition); |
214 | // Rebalancing on both sides isn't necessary if we've inserted only spaces. |
215 | if (!shouldRebalanceLeadingWhitespaceFor(m_text)) |
216 | rebalanceWhitespaceAt(startPosition); |
217 | } else { |
218 | ASSERT(m_rebalanceType == RebalanceAllWhitespaces); |
219 | if (canRebalance(startPosition) && canRebalance(endPosition)) |
220 | rebalanceWhitespaceOnTextSubstring(*textNode, startPosition.offsetInContainerNode(), endPosition.offsetInContainerNode()); |
221 | } |
222 | } |
223 | |
224 | setEndingSelectionWithoutValidation(startPosition, endPosition); |
225 | |
226 | // Handle the case where there is a typing style. |
227 | if (RefPtr<EditingStyle> typingStyle = frame().selection().typingStyle()) { |
228 | typingStyle->prepareToApplyAt(endPosition, EditingStyle::PreserveWritingDirection); |
229 | if (!typingStyle->isEmpty()) |
230 | applyStyle(typingStyle.get()); |
231 | } |
232 | |
233 | if (!m_selectInsertedText) |
234 | setEndingSelection(VisibleSelection(endingSelection().end(), endingSelection().affinity(), endingSelection().isDirectional())); |
235 | } |
236 | |
237 | Position InsertTextCommand::insertTab(const Position& pos) |
238 | { |
239 | Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent(); |
240 | if (insertPos.isNull()) |
241 | return pos; |
242 | |
243 | Node* node = insertPos.containerNode(); |
244 | unsigned int offset = node->isTextNode() ? insertPos.offsetInContainerNode() : 0; |
245 | |
246 | // keep tabs coalesced in tab span |
247 | if (isTabSpanTextNode(node)) { |
248 | Ref<Text> textNode = downcast<Text>(*node); |
249 | insertTextIntoNode(textNode, offset, "\t" ); |
250 | return Position(textNode.ptr(), offset + 1); |
251 | } |
252 | |
253 | // create new tab span |
254 | auto spanNode = createTabSpanElement(document()); |
255 | auto* spanNodePtr = spanNode.ptr(); |
256 | |
257 | // place it |
258 | if (!is<Text>(*node)) |
259 | insertNodeAt(WTFMove(spanNode), insertPos); |
260 | else { |
261 | Ref<Text> textNode = downcast<Text>(*node); |
262 | if (offset >= textNode->length()) |
263 | insertNodeAfter(WTFMove(spanNode), textNode); |
264 | else { |
265 | // split node to make room for the span |
266 | // NOTE: splitTextNode uses textNode for the |
267 | // second node in the split, so we need to |
268 | // insert the span before it. |
269 | if (offset > 0) |
270 | splitTextNode(textNode, offset); |
271 | insertNodeBefore(WTFMove(spanNode), textNode); |
272 | } |
273 | } |
274 | |
275 | // return the position following the new tab |
276 | return lastPositionInNode(spanNodePtr); |
277 | } |
278 | |
279 | } |
280 | |