• 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 "DocumentFragment.h"
37 #include "SplitElementCommand.h"
38 #include "TextIterator.h"
39 #include "htmlediting.h"
40 #include "visible_units.h"
41 #include <wtf/StdLibExtras.h>
42 
43 namespace WebCore {
44 
45 using namespace HTMLNames;
46 
indentBlockquoteString()47 static String indentBlockquoteString()
48 {
49     DEFINE_STATIC_LOCAL(String, string, ("webkit-indent-blockquote"));
50     return string;
51 }
52 
createIndentBlockquoteElement(Document * document)53 static PassRefPtr<HTMLBlockquoteElement> createIndentBlockquoteElement(Document* document)
54 {
55     RefPtr<HTMLBlockquoteElement> element = new HTMLBlockquoteElement(blockquoteTag, document);
56     element->setAttribute(classAttr, indentBlockquoteString());
57     element->setAttribute(styleAttr, "margin: 0 0 0 40px; border: none; padding: 0px;");
58     return element.release();
59 }
60 
isListOrIndentBlockquote(const Node * node)61 static bool isListOrIndentBlockquote(const Node* node)
62 {
63     return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(blockquoteTag));
64 }
65 
IndentOutdentCommand(Document * document,EIndentType typeOfAction,int marginInPixels)66 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
67     : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
68 {
69 }
70 
tryIndentingAsListItem(const VisiblePosition & endOfCurrentParagraph)71 bool IndentOutdentCommand::tryIndentingAsListItem(const VisiblePosition& endOfCurrentParagraph)
72 {
73     // If our selection is not inside a list, bail out.
74     Node* lastNodeInSelectedParagraph = endOfCurrentParagraph.deepEquivalent().node();
75     RefPtr<Element> listNode = enclosingList(lastNodeInSelectedParagraph);
76     if (!listNode)
77         return false;
78 
79     // Find the list item enclosing the current paragraph
80     Element* selectedListItem = static_cast<Element*>(enclosingBlock(endOfCurrentParagraph.deepEquivalent().node()));
81     // FIXME: we need to deal with the case where there is no li (malformed HTML)
82     if (!selectedListItem->hasTagName(liTag))
83         return false;
84 
85     // FIXME: previousElementSibling does not ignore non-rendered content like <span></span>.  Should we?
86     Element* previousList = selectedListItem->previousElementSibling();
87     Element* nextList = selectedListItem->nextElementSibling();
88 
89     RefPtr<Element> newList = document()->createElement(listNode->tagQName(), false);
90     insertNodeBefore(newList, selectedListItem);
91     appendParagraphIntoNode(visiblePositionBeforeNode(selectedListItem), visiblePositionAfterNode(selectedListItem), newList.get());
92 
93     if (canMergeLists(previousList, newList.get()))
94         mergeIdenticalElements(previousList, newList);
95     if (canMergeLists(newList.get(), nextList))
96         mergeIdenticalElements(newList, nextList);
97 
98     return true;
99 }
100 
indentIntoBlockquote(const VisiblePosition & startOfCurrentParagraph,const VisiblePosition & endOfCurrentParagraph,RefPtr<Element> & targetBlockquote,Node * nodeToSplitTo)101 void IndentOutdentCommand::indentIntoBlockquote(const VisiblePosition& startOfCurrentParagraph, const VisiblePosition& endOfCurrentParagraph, RefPtr<Element>& targetBlockquote, Node* nodeToSplitTo)
102 {
103     Node* enclosingCell = 0;
104 
105     if (!targetBlockquote) {
106         // Create a new blockquote and insert it as a child of the enclosing block element. We accomplish
107         // this by splitting all parents of the current paragraph up to that point.
108         targetBlockquote = createIndentBlockquoteElement(document());
109         if (isTableCell(nodeToSplitTo))
110             enclosingCell = nodeToSplitTo;
111         RefPtr<Node> startOfNewBlock = splitTreeToNode(startOfCurrentParagraph.deepEquivalent().node(), nodeToSplitTo);
112         insertNodeBefore(targetBlockquote, startOfNewBlock);
113     }
114 
115     VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
116     appendParagraphIntoNode(startOfCurrentParagraph, endOfCurrentParagraph, targetBlockquote.get());
117 
118     // Don't put the next paragraph in the blockquote we just created for this paragraph unless
119     // the next paragraph is in the same cell.
120     if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
121         targetBlockquote = 0;
122 }
123 
isAtUnsplittableElement(const Position & pos) const124 bool IndentOutdentCommand::isAtUnsplittableElement(const Position& pos) const
125 {
126     Node* node = pos.node();
127     return node == editableRootForPosition(pos) || node == enclosingNodeOfType(pos, &isTableCell);
128 }
129 
130 // Enclose all nodes between start and end by newParent, which is a sibling node of nodes between start and end
131 // FIXME: moveParagraph is overly complicated.  We need to clean up moveParagraph so that it uses appendParagraphIntoNode
132 // or prepare more specialized functions and delete moveParagraph
appendParagraphIntoNode(const VisiblePosition & start,const VisiblePosition & end,Node * newParent)133 void IndentOutdentCommand::appendParagraphIntoNode(const VisiblePosition& start, const VisiblePosition& end, Node* newParent)
134 {
135     ASSERT(newParent);
136     ASSERT(newParent->isContentEditable());
137     ASSERT(isStartOfParagraph(start) && isEndOfParagraph(end));
138 
139     Position endOfParagraph = end.deepEquivalent().downstream();
140     Node* insertionPoint = newParent->lastChild();// Remember the place to put br later
141     // Look for the beginning of the last paragraph in newParent
142     Node* startOfLastParagraph = startOfParagraph(Position(newParent, newParent->childNodeCount())).deepEquivalent().node();
143     if (startOfLastParagraph && !startOfLastParagraph->isDescendantOf(newParent))
144         startOfLastParagraph = 0;
145 
146     // Extend the range so that we can append wrapping nodes as well if they're containd within the paragraph
147     ExceptionCode ec = 0;
148     RefPtr<Range> selectedRange = createRange(document(), start, end, ec);
149     RefPtr<Range> extendedRange = extendRangeToWrappingNodes(selectedRange, selectedRange.get(), newParent->parentNode());
150     newParent->appendChild(extendedRange->extractContents(ec), ec);
151 
152     // If the start of paragraph didn't change by appending nodes, we should insert br to seperate the paragraphs.
153     Node* startOfNewParagraph = startOfParagraph(Position(newParent, newParent->childNodeCount())).deepEquivalent().node();
154     if (startOfNewParagraph == startOfLastParagraph) {
155         if (insertionPoint)
156             newParent->insertBefore(createBreakElement(document()), insertionPoint->nextSibling(), ec);
157         else
158             newParent->appendChild(createBreakElement(document()), ec);
159     }
160 
161     // Remove unnecessary br from the place where we moved the paragraph from
162     removeUnnecessaryLineBreakAt(endOfParagraph);
163 }
164 
removeUnnecessaryLineBreakAt(const Position & endOfParagraph)165 void IndentOutdentCommand::removeUnnecessaryLineBreakAt(const Position& endOfParagraph)
166 {
167     // If there is something in this paragraph, then don't remove br.
168     if (!isStartOfParagraph(endOfParagraph) || !isEndOfParagraph(endOfParagraph))
169         return;
170 
171     // We only care about br at the end of paragraph
172     Node* br = endOfParagraph.node();
173     Node* parentNode = br->parentNode();
174 
175     // If the node isn't br or the parent node is empty, then don't remove.
176     if (!br->hasTagName(brTag) || isVisiblyAdjacent(positionBeforeNode(parentNode), positionAfterNode(parentNode)))
177         return;
178 
179     removeNodeAndPruneAncestors(br);
180 }
181 
indentRegion()182 void IndentOutdentCommand::indentRegion()
183 {
184     VisibleSelection selection = selectionForParagraphIteration(endingSelection());
185     VisiblePosition startOfSelection = selection.visibleStart();
186     VisiblePosition endOfSelection = selection.visibleEnd();
187     RefPtr<Range> selectedRange = selection.firstRange();
188 
189     ASSERT(!startOfSelection.isNull());
190     ASSERT(!endOfSelection.isNull());
191 
192     // Special case empty unsplittable elements because there's nothing to split
193     // and there's nothing to move.
194     Position start = startOfSelection.deepEquivalent().downstream();
195     if (isAtUnsplittableElement(start)) {
196         RefPtr<Element> blockquote = createIndentBlockquoteElement(document());
197         insertNodeAt(blockquote, start);
198         RefPtr<Element> placeholder = createBreakElement(document());
199         appendNode(placeholder, blockquote);
200         setEndingSelection(VisibleSelection(Position(placeholder.get(), 0), DOWNSTREAM));
201         return;
202     }
203 
204     RefPtr<Element> blockquoteForNextIndent;
205     VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
206     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
207     while (endOfCurrentParagraph != endAfterSelection) {
208         // Iterate across the selected paragraphs...
209         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
210         if (tryIndentingAsListItem(endOfCurrentParagraph))
211             blockquoteForNextIndent = 0;
212         else {
213             VisiblePosition startOfCurrentParagraph = startOfParagraph(endOfCurrentParagraph);
214             Node* blockNode = enclosingBlock(endOfCurrentParagraph.deepEquivalent().node());
215             // extend the region so that it contains all the ancestor blocks within the selection
216             ExceptionCode ec;
217             Element* unsplittableNode = unsplittableElementForPosition(endOfCurrentParagraph.deepEquivalent());
218             RefPtr<Range> originalRange = createRange(document(), endOfCurrentParagraph, endOfCurrentParagraph, ec);
219             RefPtr<Range> extendedRange = extendRangeToWrappingNodes(originalRange, selectedRange.get(), unsplittableNode);
220             if (originalRange != extendedRange) {
221                 ExceptionCode ec = 0;
222                 endOfCurrentParagraph = endOfParagraph(extendedRange->endPosition().previous());
223                 blockNode = enclosingBlock(extendedRange->commonAncestorContainer(ec));
224             }
225 
226             endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
227             indentIntoBlockquote(startOfCurrentParagraph, endOfCurrentParagraph, blockquoteForNextIndent, blockNode);
228             // blockquoteForNextIndent will be updated in the function
229         }
230         // Sanity check: Make sure our moveParagraph calls didn't remove endOfNextParagraph.deepEquivalent().node()
231         // If somehow we did, return to prevent crashes.
232         if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
233             ASSERT_NOT_REACHED();
234             return;
235         }
236         endOfCurrentParagraph = endOfNextParagraph;
237     }
238 
239 }
240 
outdentParagraph()241 void IndentOutdentCommand::outdentParagraph()
242 {
243     VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
244     VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
245 
246     Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
247     if (!enclosingNode || !enclosingNode->parentNode()->isContentEditable())  // We can't outdent if there is no place to go!
248         return;
249 
250     // Use InsertListCommand to remove the selection from the list
251     if (enclosingNode->hasTagName(olTag)) {
252         applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList));
253         return;
254     }
255     if (enclosingNode->hasTagName(ulTag)) {
256         applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList));
257         return;
258     }
259 
260     // The selection is inside a blockquote i.e. enclosingNode is a blockquote
261     VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
262     VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
263     VisiblePosition lastPositionInEnclosingBlock = VisiblePosition(Position(enclosingNode, enclosingNode->childNodeCount()));
264     VisiblePosition endOfEnclosingBlock = endOfBlock(lastPositionInEnclosingBlock);
265     if (visibleStartOfParagraph == startOfEnclosingBlock &&
266         visibleEndOfParagraph == endOfEnclosingBlock) {
267         // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
268         Node* splitPoint = enclosingNode->nextSibling();
269         removeNodePreservingChildren(enclosingNode);
270         // outdentRegion() assumes it is operating on the first paragraph of an enclosing blockquote, but if there are multiply nested blockquotes and we've
271         // just removed one, then this assumption isn't true. By splitting the next containing blockquote after this node, we keep this assumption true
272         if (splitPoint) {
273             if (Node* splitPointParent = splitPoint->parentNode()) {
274                 if (splitPointParent->hasTagName(blockquoteTag)
275                     && !splitPoint->hasTagName(blockquoteTag)
276                     && splitPointParent->parentNode()->isContentEditable()) // We can't outdent if there is no place to go!
277                     splitElement(static_cast<Element*>(splitPointParent), splitPoint);
278             }
279         }
280 
281         updateLayout();
282         visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
283         visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
284         if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
285             insertNodeAt(createBreakElement(document()), visibleStartOfParagraph.deepEquivalent());
286         if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
287             insertNodeAt(createBreakElement(document()), visibleEndOfParagraph.deepEquivalent());
288 
289         return;
290     }
291     Node* enclosingBlockFlow = enclosingBlock(visibleStartOfParagraph.deepEquivalent().node());
292     RefPtr<Node> splitBlockquoteNode = enclosingNode;
293     if (enclosingBlockFlow != enclosingNode)
294         splitBlockquoteNode = splitTreeToNode(enclosingBlockFlow, enclosingNode, true);
295     else {
296         // We split the blockquote at where we start outdenting.
297         splitElement(static_cast<Element*>(enclosingNode), visibleStartOfParagraph.deepEquivalent().node());
298     }
299     RefPtr<Node> placeholder = createBreakElement(document());
300     insertNodeBefore(placeholder, splitBlockquoteNode);
301     moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
302 }
303 
outdentRegion()304 void IndentOutdentCommand::outdentRegion()
305 {
306     VisiblePosition startOfSelection = endingSelection().visibleStart();
307     VisiblePosition endOfSelection = endingSelection().visibleEnd();
308     VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
309 
310     ASSERT(!startOfSelection.isNull());
311     ASSERT(!endOfSelection.isNull());
312 
313     if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
314         outdentParagraph();
315         return;
316     }
317 
318     Position originalSelectionEnd = endingSelection().end();
319     setEndingSelection(endingSelection().visibleStart());
320     outdentParagraph();
321     Position originalSelectionStart = endingSelection().start();
322     VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
323     VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
324     while (endOfCurrentParagraph != endAfterSelection) {
325         VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
326         if (endOfCurrentParagraph == endOfLastParagraph)
327             setEndingSelection(VisibleSelection(originalSelectionEnd, DOWNSTREAM));
328         else
329             setEndingSelection(endOfCurrentParagraph);
330         outdentParagraph();
331         endOfCurrentParagraph = endOfNextParagraph;
332     }
333     setEndingSelection(VisibleSelection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
334 }
335 
doApply()336 void IndentOutdentCommand::doApply()
337 {
338     if (endingSelection().isNone())
339         return;
340 
341     if (!endingSelection().rootEditableElement())
342         return;
343 
344     VisiblePosition visibleEnd = endingSelection().visibleEnd();
345     VisiblePosition visibleStart = endingSelection().visibleStart();
346     // When a selection ends at the start of a paragraph, we rarely paint
347     // the selection gap before that paragraph, because there often is no gap.
348     // In a case like this, it's not obvious to the user that the selection
349     // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
350     // operated on that paragraph.
351     // FIXME: We paint the gap before some paragraphs that are indented with left
352     // margin/padding, but not others.  We should make the gap painting more consistent and
353     // then use a left margin/padding rule here.
354     if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
355         setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true)));
356 
357     if (m_typeOfAction == Indent)
358         indentRegion();
359     else
360         outdentRegion();
361 }
362 
363 }
364