1 /*
2 * Copyright (C) 2006, 2008 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 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 (IndentOutdentCommandINCLUDING, 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 "IndentOutdentCommand.h"
28
29 #include "Document.h"
30 #include "Element.h"
31 #include "HTMLBlockquoteElement.h"
32 #include "HTMLNames.h"
33 #include "InsertLineBreakCommand.h"
34 #include "InsertListCommand.h"
35 #include "Range.h"
36 #include "SplitElementCommand.h"
37 #include "TextIterator.h"
38 #include "htmlediting.h"
39 #include "visible_units.h"
40 #include <wtf/StdLibExtras.h>
41
42 namespace WebCore {
43
44 using namespace HTMLNames;
45
indentBlockquoteString()46 static String indentBlockquoteString()
47 {
48 DEFINE_STATIC_LOCAL(String, string, ("webkit-indent-blockquote"));
49 return string;
50 }
51
createIndentBlockquoteElement(Document * document)52 static PassRefPtr<HTMLBlockquoteElement> createIndentBlockquoteElement(Document* document)
53 {
54 RefPtr<HTMLBlockquoteElement> element = new HTMLBlockquoteElement(blockquoteTag, document);
55 element->setAttribute(classAttr, indentBlockquoteString());
56 element->setAttribute(styleAttr, "margin: 0 0 0 40px; border: none; padding: 0px;");
57 return element.release();
58 }
59
isListOrIndentBlockquote(const Node * node)60 static bool isListOrIndentBlockquote(const Node* node)
61 {
62 return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag));
63 }
64
IndentOutdentCommand(Document * document,EIndentType typeOfAction,int marginInPixels)65 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
66 : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
67 {
68 }
69
tryIndentingAsListItem(const VisiblePosition & endOfCurrentParagraph)70 bool IndentOutdentCommand::tryIndentingAsListItem(const VisiblePosition& endOfCurrentParagraph)
71 {
72 // If our selection is not inside a list, bail out.
73 Node* lastNodeInSelectedParagraph = endOfCurrentParagraph.deepEquivalent().node();
74 RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph);
75 if (!listNode)
76 return false;
77
78 // Find the list item enclosing the current paragraph
79 Element* selectedListItem = static_cast<Element*>(enclosingBlock(lastNodeInSelectedParagraph));
80 // FIXME: enclosingBlock shouldn't return the passed in element. See the
81 // comment on the function about how to fix rather than having to adjust here.
82 if (selectedListItem == lastNodeInSelectedParagraph)
83 selectedListItem = static_cast<Element*>(enclosingBlock(lastNodeInSelectedParagraph->parentNode()));
84
85 // FIXME: we need to deal with the case where there is no li (malformed HTML)
86 if (!selectedListItem->hasTagName(liTag))
87 return false;
88
89 // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>. Should we?
90 Element* previousList = selectedListItem->previousElementSibling();
91 Element* nextList = selectedListItem->nextElementSibling();
92
93 RefPtr<Element> newList = document()->createElement(listNode->tagQName(), false);
94 insertNodeBefore(newList, selectedListItem);
95
96 moveParagraphWithClones(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, newList.get(), selectedListItem);
97
98 if (canMergeLists(previousList, newList.get()))
99 mergeIdenticalElements(previousList, newList);
100 if (canMergeLists(newList.get(), nextList))
101 mergeIdenticalElements(newList, nextList);
102
103 return true;
104 }
105
indentIntoBlockquote(const VisiblePosition & endOfCurrentParagraph,const VisiblePosition & endOfNextParagraph,RefPtr<Element> & targetBlockquote)106 void IndentOutdentCommand::indentIntoBlockquote(const VisiblePosition& endOfCurrentParagraph, const VisiblePosition& endOfNextParagraph, RefPtr<Element>& targetBlockquote)
107 {
108 Node* enclosingCell = 0;
109
110 Position start = startOfParagraph(endOfCurrentParagraph).deepEquivalent();
111 enclosingCell = enclosingNodeOfType(start, &isTableCell);
112 Node* nodeToSplitTo;
113 if (enclosingCell)
114 nodeToSplitTo = enclosingCell;
115 else if (enclosingList(start.node()))
116 nodeToSplitTo = enclosingBlock(start.node());
117 else
118 nodeToSplitTo = editableRootForPosition(start);
119
120 RefPtr<Node> outerBlock = splitTreeToNode(start.node(), nodeToSplitTo);
121
122 if (!targetBlockquote) {
123 // Create a new blockquote and insert it as a child of the root editable element. We accomplish
124 // this by splitting all parents of the current paragraph up to that point.
125 targetBlockquote = createIndentBlockquoteElement(document());
126 insertNodeBefore(targetBlockquote, outerBlock);
127 }
128
129 moveParagraphWithClones(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, targetBlockquote.get(), outerBlock.get());
130
131 // Don't put the next paragraph in the blockquote we just created for this paragraph unless
132 // the next paragraph is in the same cell.
133 if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
134 targetBlockquote = 0;
135 }
136
indentRegion(const VisiblePosition & startOfSelection,const VisiblePosition & endOfSelection)137 void IndentOutdentCommand::indentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
138 {
139 // Special case empty unsplittable elements because there's nothing to split
140 // and there's nothing to move.
141 Position start = startOfSelection.deepEquivalent().downstream();
142 if (isAtUnsplittableElement(start)) {
143 RefPtr<Element> blockquote = createIndentBlockquoteElement(document());
144 insertNodeAt(blockquote, start);
145 RefPtr<Element> placeholder = createBreakElement(document());
146 appendNode(placeholder, blockquote);
147 setEndingSelection(VisibleSelection(Position(placeholder.get(), 0), DOWNSTREAM));
148 return;
149 }
150
151 RefPtr<Element> blockquoteForNextIndent;
152 VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
153 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
154 while (endOfCurrentParagraph != endAfterSelection) {
155 // Iterate across the selected paragraphs...
156 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
157 if (tryIndentingAsListItem(endOfCurrentParagraph))
158 blockquoteForNextIndent = 0;
159 else
160 indentIntoBlockquote(endOfCurrentParagraph, endOfNextParagraph, blockquoteForNextIndent);
161
162 // indentIntoBlockquote could move more than one paragraph if the paragraph
163 // is in a list item or a table. As a result, endAfterSelection could refer to a position
164 // no longer in the document.
165 if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().node()->inDocument())
166 break;
167 // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().node()
168 // If somehow we did, return to prevent crashes.
169 if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
170 ASSERT_NOT_REACHED();
171 return;
172 }
173 endOfCurrentParagraph = endOfNextParagraph;
174 }
175 }
176
outdentParagraph()177 void IndentOutdentCommand::outdentParagraph()
178 {
179 VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
180 VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
181
182 Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
183 if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable()) // We can't outdent if there is no place to go!
184 return;
185
186 // Use InsertListCommand to remove the selection from the list
187 if (enclosingNode->hasTagName(olTag)) {
188 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList));
189 return;
190 }
191 if (enclosingNode->hasTagName(ulTag)) {
192 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList));
193 return;
194 }
195
196 // The selection is inside a blockquote i.e. enclosingNode is a blockquote
197 VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
198 VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
199 VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount()));
200 VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock);
201 if (visibleStartOfParagraph == startOfEnclosingBlock &&
202 visibleEndOfParagraph == endOfEnclosingBlock) {
203 // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
204 Node* splitPoint = enclosingNode->nextSibling();
205 removeNodePreservingChildren(enclosingNode);
206 // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've
207 // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true
208 if (splitPoint) {
209 if (Node* splitPointParent = splitPoint->parentNode()) {
210 if (splitPointParent->hasTagName(blockquoteTag)
211 && !splitPoint->hasTagName(blockquoteTag)
212 && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go!
213 splitElement(static_cast<Element*>(splitPointParent), splitPoint);
214 }
215 }
216
217 updateLayout();
218 visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
219 visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
220 if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
221 insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent());
222 if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
223 insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent());
224
225 return;
226 }
227 Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node());
228 RefPtr<Node> splitBlockquoteNode = enclosingNode;
229 if (enclosingBlockFlow != enclosingNode)
230 splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true);
231 else {
232 // We split the blockquote at where we start outdenting.
233 splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node());
234 }
235 RefPtr<Node> placeholder = createBreakElement(document());
236 insertNodeBefore(placeholder, splitBlockquoteNode);
237 moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
238 }
239
outdentRegion(const VisiblePosition & startOfSelection,const VisiblePosition & endOfSelection)240 void IndentOutdentCommand::outdentRegion(const VisiblePosition& startOfSelection, const VisiblePosition& endOfSelection)
241 {
242 VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
243
244 if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
245 outdentParagraph();
246 return;
247 }
248
249 Position originalSelectionEnd = endingSelection().end();
250 VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
251 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
252
253 while (endOfCurrentParagraph != endAfterSelection) {
254 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
255 if (endOfCurrentParagraph == endOfLastParagraph)
256 setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM));
257 else
258 setEndingSelection(endOfCurrentParagraph);
259
260 outdentParagraph();
261
262 // outdentParagraph could move more than one paragraph if the paragraph
263 // is in a list item. As a result, endAfterSelection and endOfNextParagraph
264 // could refer to positions no longer in the document.
265 if (endAfterSelection.isNotNull() && !endAfterSelection.deepEquivalent().node()->inDocument())
266 break;
267
268 if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
269 endOfCurrentParagraph = endingSelection().end();
270 endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
271 }
272 endOfCurrentParagraph = endOfNextParagraph;
273 }
274 }
275
doApply()276 void IndentOutdentCommand::doApply()
277 {
278 if (endingSelection().isNone())
279 return;
280
281 if (!endingSelection().rootEditableElement())
282 return;
283
284 VisiblePosition visibleEnd = endingSelection().visibleEnd();
285 VisiblePosition visibleStart = endingSelection().visibleStart();
286 // When a selection ends at the start of a paragraph, we rarely paint
287 // the selection gap before that paragraph, because there often is no gap.
288 // In a case like this, it's not obvious to the user that the selection
289 // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
290 // operated on that paragraph.
291 // FIXME: We paint the gap before some paragraphs that are indented with left
292 // margin/padding, but not others. We should make the gap painting more consistent and
293 // then use a left margin/padding rule here.
294 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
295 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
296
297 VisibleSelection selection = selectionForParagraphIteration(endingSelection());
298 VisiblePosition startOfSelection = selection.visibleStart();
299 VisiblePosition endOfSelection = selection.visibleEnd();
300
301 int startIndex = indexForVisiblePosition(startOfSelection);
302 int endIndex = indexForVisiblePosition(endOfSelection);
303
304 ASSERT(!startOfSelection.isNull());
305 ASSERT(!endOfSelection.isNull());
306
307 if (m_typeOfAction == Indent)
308 indentRegion(startOfSelection, endOfSelection);
309 else
310 outdentRegion(startOfSelection, endOfSelection);
311
312 updateLayout();
313
314 RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true);
315 RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true);
316 if (startRange && endRange)
317 setEndingSelection(VisibleSelection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM));
318 }
319
320 }
321