2 * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
28 #include "IndentOutdentCommand.h"
29 #include "InsertListCommand.h"
31 #include "htmlediting.h"
32 #include "HTMLElement.h"
33 #include "HTMLNames.h"
34 #include "InsertLineBreakCommand.h"
36 #include "SplitElementCommand.h"
37 #include "TextIterator.h"
38 #include "visible_units.h"
42 using namespace HTMLNames
;
44 static String
indentBlockquoteString()
46 static String string
= "webkit-indent-blockquote";
50 static PassRefPtr
<Element
> createIndentBlockquoteElement(Document
* document
)
52 RefPtr
<Element
> indentBlockquoteElement
= createElement(document
, "blockquote");
53 indentBlockquoteElement
->setAttribute(classAttr
, indentBlockquoteString());
54 indentBlockquoteElement
->setAttribute(styleAttr
, "margin: 0 0 0 40px; border: none; padding: 0px;");
55 return indentBlockquoteElement
.release();
58 static bool isIndentBlockquote(const Node
* node
)
60 if (!node
|| !node
->hasTagName(blockquoteTag
) || !node
->isElementNode())
63 const Element
* elem
= static_cast<const Element
*>(node
);
64 return elem
->getAttribute(classAttr
) == indentBlockquoteString();
67 static bool isListOrIndentBlockquote(const Node
* node
)
69 return node
&& (node
->hasTagName(ulTag
) || node
->hasTagName(olTag
) || isIndentBlockquote(node
));
72 IndentOutdentCommand::IndentOutdentCommand(Document
* document
, EIndentType typeOfAction
, int marginInPixels
)
73 : CompositeEditCommand(document
), m_typeOfAction(typeOfAction
), m_marginInPixels(marginInPixels
)
76 // This function is a workaround for moveParagraph's tendency to strip blockquotes. It updates lastBlockquote to point to the
77 // correct level for the current paragraph, and returns a pointer to a placeholder br where the insertion should be performed.
78 Node
* IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition
& currentParagraph
, Node
** lastBlockquote
)
80 int currentBlockquoteLevel
= 0;
81 int lastBlockquoteLevel
= 0;
82 Node
* node
= currentParagraph
.deepEquivalent().node();
83 while ((node
= enclosingNodeOfType(Position(node
->parentNode(), 0), &isIndentBlockquote
)))
84 currentBlockquoteLevel
++;
85 node
= *lastBlockquote
;
86 while ((node
= enclosingNodeOfType(Position(node
->parentNode(), 0), &isIndentBlockquote
)))
87 lastBlockquoteLevel
++;
88 while (currentBlockquoteLevel
> lastBlockquoteLevel
) {
89 RefPtr
<Node
> newBlockquote
= createIndentBlockquoteElement(document());
90 appendNode(newBlockquote
.get(), *lastBlockquote
);
91 *lastBlockquote
= newBlockquote
.get();
92 lastBlockquoteLevel
++;
94 while (currentBlockquoteLevel
< lastBlockquoteLevel
) {
95 *lastBlockquote
= enclosingNodeOfType(Position((*lastBlockquote
)->parentNode(), 0), &isIndentBlockquote
);
96 lastBlockquoteLevel
--;
98 RefPtr
<Node
> placeholder
= createBreakElement(document());
99 appendNode(placeholder
.get(), *lastBlockquote
);
100 // Add another br before the placeholder if it collapsed.
101 VisiblePosition
visiblePos(Position(placeholder
.get(), 0));
102 if (!isStartOfParagraph(visiblePos
))
103 insertNodeBefore(createBreakElement(document()).get(), placeholder
.get());
104 return placeholder
.get();
107 void IndentOutdentCommand::indentRegion()
109 Selection selection
= selectionForParagraphIteration(endingSelection());
110 VisiblePosition startOfSelection
= selection
.visibleStart();
111 VisiblePosition endOfSelection
= selection
.visibleEnd();
112 int startIndex
= indexForVisiblePosition(startOfSelection
);
113 int endIndex
= indexForVisiblePosition(endOfSelection
);
115 ASSERT(!startOfSelection
.isNull());
116 ASSERT(!endOfSelection
.isNull());
118 // Special case empty root editable elements because there's nothing to split
119 // and there's nothing to move.
120 Position start
= startOfSelection
.deepEquivalent().downstream();
121 if (start
.node() == editableRootForPosition(start
)) {
122 RefPtr
<Node
> blockquote
= createIndentBlockquoteElement(document());
123 insertNodeAt(blockquote
.get(), start
);
124 RefPtr
<Node
> placeholder
= createBreakElement(document());
125 appendNode(placeholder
.get(), blockquote
.get());
126 setEndingSelection(Selection(Position(placeholder
.get(), 0), DOWNSTREAM
));
130 Node
* previousListNode
= 0;
131 Node
* newListNode
= 0;
132 Node
* newBlockquote
= 0;
133 VisiblePosition endOfCurrentParagraph
= endOfParagraph(startOfSelection
);
134 VisiblePosition endAfterSelection
= endOfParagraph(endOfParagraph(endOfSelection
).next());
135 while (endOfCurrentParagraph
!= endAfterSelection
) {
136 // Iterate across the selected paragraphs...
137 VisiblePosition endOfNextParagraph
= endOfParagraph(endOfCurrentParagraph
.next());
138 Node
* listNode
= enclosingList(endOfCurrentParagraph
.deepEquivalent().node());
139 Node
* insertionPoint
;
141 RefPtr
<Node
> placeholder
= createBreakElement(document());
142 insertionPoint
= placeholder
.get();
144 RefPtr
<Node
> listItem
= createListItemElement(document());
145 if (listNode
== previousListNode
) {
146 // The previous paragraph was inside the same list, so add this list item to the list we already created
147 appendNode(listItem
.get(), newListNode
);
148 appendNode(placeholder
.get(), listItem
.get());
150 // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
151 RefPtr
<Node
> clonedList
= static_cast<Element
*>(listNode
)->cloneNode(false);
152 insertNodeBefore(clonedList
.get(), enclosingListChild(endOfCurrentParagraph
.deepEquivalent().node()));
153 appendNode(listItem
.get(), clonedList
.get());
154 appendNode(placeholder
.get(), listItem
.get());
155 newListNode
= clonedList
.get();
156 previousListNode
= listNode
;
158 } else if (newBlockquote
)
159 // The previous paragraph was put into a new blockquote, so move this paragraph there as well
160 insertionPoint
= prepareBlockquoteLevelForInsertion(endOfCurrentParagraph
, &newBlockquote
);
162 // Create a new blockquote and insert it as a child of the root editable element. We accomplish
163 // this by splitting all parents of the current paragraph up to that point.
164 RefPtr
<Node
> blockquote
= createIndentBlockquoteElement(document());
165 Position start
= startOfParagraph(endOfCurrentParagraph
).deepEquivalent();
167 Node
* enclosingCell
= enclosingNodeOfType(start
, &isTableCell
);
168 Node
* nodeToSplitTo
= enclosingCell
? enclosingCell
: editableRootForPosition(start
);
169 RefPtr
<Node
> startOfNewBlock
= splitTreeToNode(start
.node(), nodeToSplitTo
);
170 insertNodeBefore(blockquote
.get(), startOfNewBlock
.get());
171 newBlockquote
= blockquote
.get();
172 insertionPoint
= prepareBlockquoteLevelForInsertion(endOfCurrentParagraph
, &newBlockquote
);
173 // Don't put the next paragraph in the blockquote we just created for this paragraph unless
174 // the next paragraph is in the same cell.
175 if (enclosingCell
&& enclosingCell
!= enclosingNodeOfType(endOfNextParagraph
.deepEquivalent(), &isTableCell
))
178 moveParagraph(startOfParagraph(endOfCurrentParagraph
), endOfCurrentParagraph
, VisiblePosition(Position(insertionPoint
, 0)), true);
179 // moveParagraph should not destroy content that contains endOfNextParagraph, but if it does, return here
181 if (endOfNextParagraph
.isNotNull() && !endOfNextParagraph
.deepEquivalent().node()->inDocument()) {
182 ASSERT_NOT_REACHED();
185 endOfCurrentParagraph
= endOfNextParagraph
;
188 RefPtr
<Range
> startRange
= TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex
, 0, true);
189 RefPtr
<Range
> endRange
= TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex
, 0, true);
190 if (startRange
&& endRange
)
191 setEndingSelection(Selection(startRange
->startPosition(), endRange
->startPosition(), DOWNSTREAM
));
194 void IndentOutdentCommand::outdentParagraph()
196 VisiblePosition visibleStartOfParagraph
= startOfParagraph(endingSelection().visibleStart());
197 VisiblePosition visibleEndOfParagraph
= endOfParagraph(visibleStartOfParagraph
);
199 Node
* enclosingNode
= enclosingNodeOfType(visibleStartOfParagraph
.deepEquivalent(), &isListOrIndentBlockquote
);
203 // Use InsertListCommand to remove the selection from the list
204 if (enclosingNode
->hasTagName(olTag
)) {
205 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList
, ""));
208 if (enclosingNode
->hasTagName(ulTag
)) {
209 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList
, ""));
213 // The selection is inside a blockquote
214 VisiblePosition positionInEnclosingBlock
= VisiblePosition(Position(enclosingNode
, 0));
215 VisiblePosition startOfEnclosingBlock
= startOfBlock(positionInEnclosingBlock
);
216 VisiblePosition endOfEnclosingBlock
= endOfBlock(positionInEnclosingBlock
);
217 if (visibleStartOfParagraph
== startOfEnclosingBlock
&&
218 visibleEndOfParagraph
== endOfEnclosingBlock
) {
219 // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
220 removeNodePreservingChildren(enclosingNode
);
222 visibleStartOfParagraph
= VisiblePosition(visibleStartOfParagraph
.deepEquivalent());
223 visibleEndOfParagraph
= VisiblePosition(visibleEndOfParagraph
.deepEquivalent());
224 if (visibleStartOfParagraph
.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph
))
225 insertNodeAt(createBreakElement(document()).get(), visibleStartOfParagraph
.deepEquivalent());
226 if (visibleEndOfParagraph
.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph
))
227 insertNodeAt(createBreakElement(document()).get(), visibleEndOfParagraph
.deepEquivalent());
230 Node
* enclosingBlockFlow
= enclosingBlockFlowElement(visibleStartOfParagraph
);
231 RefPtr
<Node
> splitBlockquoteNode
= enclosingNode
;
232 if (enclosingBlockFlow
!= enclosingNode
)
233 splitBlockquoteNode
= splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph
), enclosingNode
, true);
234 RefPtr
<Node
> placeholder
= createBreakElement(document());
235 insertNodeBefore(placeholder
.get(), splitBlockquoteNode
.get());
236 moveParagraph(startOfParagraph(visibleStartOfParagraph
), endOfParagraph(visibleEndOfParagraph
), VisiblePosition(Position(placeholder
.get(), 0)), true);
239 void IndentOutdentCommand::outdentRegion()
241 VisiblePosition startOfSelection
= endingSelection().visibleStart();
242 VisiblePosition endOfSelection
= endingSelection().visibleEnd();
243 VisiblePosition endOfLastParagraph
= endOfParagraph(endOfSelection
);
245 ASSERT(!startOfSelection
.isNull());
246 ASSERT(!endOfSelection
.isNull());
248 if (endOfParagraph(startOfSelection
) == endOfLastParagraph
) {
253 Position originalSelectionEnd
= endingSelection().end();
254 setEndingSelection(endingSelection().visibleStart());
256 Position originalSelectionStart
= endingSelection().start();
257 VisiblePosition endOfCurrentParagraph
= endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
258 VisiblePosition endAfterSelection
= endOfParagraph(endOfParagraph(endOfSelection
).next());
259 while (endOfCurrentParagraph
!= endAfterSelection
) {
260 VisiblePosition endOfNextParagraph
= endOfParagraph(endOfCurrentParagraph
.next());
261 if (endOfCurrentParagraph
== endOfLastParagraph
)
262 setEndingSelection(Selection(originalSelectionEnd
, DOWNSTREAM
));
264 setEndingSelection(endOfCurrentParagraph
);
266 endOfCurrentParagraph
= endOfNextParagraph
;
268 setEndingSelection(Selection(originalSelectionStart
, endingSelection().end(), DOWNSTREAM
));
271 void IndentOutdentCommand::doApply()
273 if (endingSelection().isNone())
276 if (!endingSelection().rootEditableElement())
279 VisiblePosition visibleEnd
= endingSelection().visibleEnd();
280 VisiblePosition visibleStart
= endingSelection().visibleStart();
281 // When a selection ends at the start of a paragraph, we rarely paint
282 // the selection gap before that paragraph, because there often is no gap.
283 // In a case like this, it's not obvious to the user that the selection
284 // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
285 // operated on that paragraph.
286 // FIXME: We paint the gap before some paragraphs that are indented with left
287 // margin/padding, but not others. We should make the gap painting more consistent and
288 // then use a left margin/padding rule here.
289 if (visibleEnd
!= visibleStart
&& isStartOfParagraph(visibleEnd
))
290 setEndingSelection(Selection(visibleStart
, visibleEnd
.previous(true)));
292 if (m_typeOfAction
== Indent
)