• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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