Reviewed by Justin Garcia.
[WebKit-https.git] / WebCore / editing / InsertTextCommand.cpp
1 /*
2  * Copyright (C) 2005 Apple Computer, 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 COMPUTER, 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 COMPUTER, 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 "CharacterNames.h"
30 #include "CSSMutableStyleDeclaration.h"
31 #include "CSSComputedStyleDeclaration.h"
32 #include "Document.h"
33 #include "Element.h"
34 #include "EditingText.h"
35 #include "Editor.h"
36 #include "Frame.h"
37 #include "Logging.h"
38 #include "HTMLInterchange.h"
39 #include "htmlediting.h"
40 #include "TextIterator.h"
41 #include "TypingCommand.h"
42 #include "visible_units.h"
43
44 namespace WebCore {
45
46 InsertTextCommand::InsertTextCommand(Document *document) 
47     : CompositeEditCommand(document), m_charactersAdded(0)
48 {
49 }
50
51 void InsertTextCommand::doApply()
52 {
53 }
54
55 Position InsertTextCommand::prepareForTextInsertion(const Position& p)
56 {
57     Position pos = p;
58     // If an anchor was removed and the selection hasn't changed, we restore it.
59     RefPtr<Node> anchor = document()->frame()->editor()->removedAnchor();
60     if (anchor) {
61         insertNodeAt(anchor.get(), pos);
62         document()->frame()->editor()->setRemovedAnchor(0);
63         pos = Position(anchor.get(), 0);
64     }
65     // Prepare for text input by looking at the specified position.
66     // It may be necessary to insert a text node to receive characters.
67     if (!pos.node()->isTextNode()) {
68         RefPtr<Node> textNode = document()->createEditingTextNode("");
69         insertNodeAt(textNode.get(), pos);
70         return Position(textNode.get(), 0);
71     }
72
73     if (isTabSpanTextNode(pos.node())) {
74         RefPtr<Node> textNode = document()->createEditingTextNode("");
75         insertNodeAtTabSpanPosition(textNode.get(), pos);
76         return Position(textNode.get(), 0);
77     }
78
79     return pos;
80 }
81
82 void InsertTextCommand::input(const String& originalText, bool selectInsertedText)
83 {
84     String text = originalText;
85     
86     ASSERT(text.find('\n') == -1);
87
88     if (endingSelection().isNone())
89         return;
90         
91     if (RenderObject* renderer = endingSelection().start().node()->renderer())
92         if (renderer->style()->collapseWhiteSpace())
93             // Turn all spaces into non breaking spaces, to make sure that they are treated
94             // literally, and aren't collapsed after insertion. They will be rebalanced 
95             // (turned into a sequence of regular and non breaking spaces) below.
96             text.replace(' ', noBreakSpace);
97     
98     // Delete the current selection.
99     // FIXME: This delete operation blows away the typing style.
100     if (endingSelection().isRange())
101         deleteSelection(false, true, true, false);
102     
103     // Insert the character at the leftmost candidate.
104     Position startPosition = endingSelection().start().upstream();
105     // It is possible for the node that contains startPosition to contain only unrendered whitespace,
106     // and so deleteInsignificantText could remove it.  Save the position before the node in case that happens.
107     Position positionBeforeStartNode(positionBeforeNode(startPosition.node()));
108     deleteInsignificantText(startPosition.upstream(), startPosition.downstream());
109     if (!startPosition.node()->inDocument())
110         startPosition = positionBeforeStartNode;
111     if (!startPosition.isCandidate())
112         startPosition = startPosition.downstream();
113     
114     // FIXME: This typing around anchor behavior doesn't exactly match TextEdit.  In TextEdit,
115     // you won't be placed inside a link when typing after it if you've just placed the caret
116     // there with the mouse.
117     startPosition = positionAvoidingSpecialElementBoundary(startPosition, false);
118     
119     Position endPosition;
120     
121     if (text == "\t") {
122         endPosition = insertTab(startPosition);
123         startPosition = endPosition.previous();
124         removePlaceholderAt(VisiblePosition(startPosition));
125         m_charactersAdded += 1;
126     } else {
127         // Make sure the document is set up to receive text
128         startPosition = prepareForTextInsertion(startPosition);
129         removePlaceholderAt(VisiblePosition(startPosition));
130         Text *textNode = static_cast<Text *>(startPosition.node());
131         int offset = startPosition.offset();
132
133         insertTextIntoNode(textNode, offset, text);
134         endPosition = Position(textNode, offset + text.length());
135
136         // The insertion may require adjusting adjacent whitespace, if it is present.
137         rebalanceWhitespaceAt(endPosition);
138         // Rebalancing on both sides isn't necessary if we've inserted a space.
139         if (originalText != " ") 
140             rebalanceWhitespaceAt(startPosition);
141             
142         m_charactersAdded += text.length();
143     }
144
145     // We could have inserted a part of composed character sequence,
146     // so we are basically treating ending selection as a range to avoid validation.
147     // <http://bugs.webkit.org/show_bug.cgi?id=15781>
148     Selection forcedEndingSelection;
149     forcedEndingSelection.setWithoutValidation(startPosition, endPosition);
150     setEndingSelection(forcedEndingSelection);
151
152     // Handle the case where there is a typing style.
153     // FIXME: Improve typing style.
154     // See this bug: <rdar://problem/3769899> Implementation of typing style needs improvement
155     CSSMutableStyleDeclaration* typingStyle = document()->frame()->typingStyle();
156     RefPtr<CSSComputedStyleDeclaration> endingStyle = endPosition.computedStyle();
157     endingStyle->diff(typingStyle);
158     if (typingStyle && typingStyle->length() > 0)
159         applyStyle(typingStyle);
160
161     if (!selectInsertedText)
162         setEndingSelection(Selection(endingSelection().end(), endingSelection().affinity()));
163 }
164
165 Position InsertTextCommand::insertTab(const Position& pos)
166 {
167     Position insertPos = VisiblePosition(pos, DOWNSTREAM).deepEquivalent();
168         
169     Node *node = insertPos.node();
170     unsigned int offset = insertPos.offset();
171
172     // keep tabs coalesced in tab span
173     if (isTabSpanTextNode(node)) {
174         insertTextIntoNode(static_cast<Text *>(node), offset, "\t");
175         return Position(node, offset + 1);
176     }
177     
178     // create new tab span
179     RefPtr<Element> spanNode = createTabSpanElement(document());
180     
181     // place it
182     if (!node->isTextNode()) {
183         insertNodeAt(spanNode.get(), insertPos);
184     } else {
185         Text *textNode = static_cast<Text *>(node);
186         if (offset >= textNode->length()) {
187             insertNodeAfter(spanNode.get(), textNode);
188         } else {
189             // split node to make room for the span
190             // NOTE: splitTextNode uses textNode for the
191             // second node in the split, so we need to
192             // insert the span before it.
193             if (offset > 0)
194                 splitTextNode(textNode, offset);
195             insertNodeBefore(spanNode.get(), textNode);
196         }
197     }
198     
199     // return the position following the new tab
200     return Position(spanNode->lastChild(), caretMaxOffset(spanNode->lastChild()));
201 }
202
203 bool InsertTextCommand::isInsertTextCommand() const
204 {
205     return true;
206 }
207
208 }