1 /* 2 * Copyright (C) 2006 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 "Element.h" 28 #include "InsertListCommand.h" 29 #include "DocumentFragment.h" 30 #include "htmlediting.h" 31 #include "HTMLElement.h" 32 #include "HTMLNames.h" 33 #include "TextIterator.h" 34 #include "visible_units.h" 35 36 namespace WebCore { 37 38 using namespace HTMLNames; 39 40 PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type) 41 { 42 RefPtr<InsertListCommand> insertCommand = new InsertListCommand(document, type); 43 insertCommand->apply(); 44 return insertCommand->m_listElement; 45 } 46 47 HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node) 48 { 49 RefPtr<HTMLElement> listElement = createUnorderedListElement(document()); 50 insertNodeBefore(listElement, node); 51 removeNode(node); 52 appendNode(node, listElement); 53 m_listElement = listElement; 54 return listElement.get(); 55 } 56 57 InsertListCommand::InsertListCommand(Document* document, Type type) 58 : CompositeEditCommand(document), m_type(type), m_forceCreateList(false) 59 { 60 } 61 62 bool InsertListCommand::modifyRange() 63 { 64 VisibleSelection selection = selectionForParagraphIteration(endingSelection()); 65 ASSERT(selection.isRange()); 66 VisiblePosition startOfSelection = selection.visibleStart(); 67 VisiblePosition endOfSelection = selection.visibleEnd(); 68 VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection); 69 70 if (startOfParagraph(startOfSelection) == startOfLastParagraph) 71 return false; 72 73 Node* startList = enclosingList(startOfSelection.deepEquivalent().node()); 74 Node* endList = enclosingList(endOfSelection.deepEquivalent().node()); 75 if (!startList || startList != endList) 76 m_forceCreateList = true; 77 78 setEndingSelection(startOfSelection); 79 doApply(); 80 // Fetch the start of the selection after moving the first paragraph, 81 // because moving the paragraph will invalidate the original start. 82 // We'll use the new start to restore the original selection after 83 // we modified all selected paragraphs. 84 startOfSelection = endingSelection().visibleStart(); 85 VisiblePosition startOfCurrentParagraph = startOfNextParagraph(startOfSelection); 86 while (startOfCurrentParagraph != startOfLastParagraph) { 87 // doApply() may operate on and remove the last paragraph of the selection from the document 88 // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an 89 // infinite loop and because there is no more work to be done. 90 // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute 91 // the new location of endOfSelection and use it as the end of the new selection. 92 if (!startOfLastParagraph.deepEquivalent().node()->inDocument()) 93 return true; 94 setEndingSelection(startOfCurrentParagraph); 95 doApply(); 96 startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); 97 } 98 setEndingSelection(endOfSelection); 99 doApply(); 100 // Fetch the end of the selection, for the reason mentioned above. 101 endOfSelection = endingSelection().visibleEnd(); 102 setEndingSelection(VisibleSelection(startOfSelection, endOfSelection)); 103 m_forceCreateList = false; 104 return true; 105 } 106 107 void InsertListCommand::doApply() 108 { 109 if (endingSelection().isNone()) 110 return; 111 112 if (!endingSelection().rootEditableElement()) 113 return; 114 115 VisiblePosition visibleEnd = endingSelection().visibleEnd(); 116 VisiblePosition visibleStart = endingSelection().visibleStart(); 117 // When a selection ends at the start of a paragraph, we rarely paint 118 // the selection gap before that paragraph, because there often is no gap. 119 // In a case like this, it's not obvious to the user that the selection 120 // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List 121 // operated on that paragraph. 122 // FIXME: We paint the gap before some paragraphs that are indented with left 123 // margin/padding, but not others. We should make the gap painting more consistent and 124 // then use a left margin/padding rule here. 125 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) 126 setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true))); 127 128 if (endingSelection().isRange() && modifyRange()) 129 return; 130 131 // FIXME: This will produce unexpected results for a selection that starts just before a 132 // table and ends inside the first cell, selectionForParagraphIteration should probably 133 // be renamed and deployed inside setEndingSelection(). 134 Node* selectionNode = endingSelection().start().node(); 135 const QualifiedName listTag = (m_type == OrderedList) ? olTag : ulTag; 136 Node* listChildNode = enclosingListChild(selectionNode); 137 bool switchListType = false; 138 if (listChildNode) { 139 // Remove the list chlild. 140 HTMLElement* listNode = enclosingList(listChildNode); 141 if (!listNode) 142 listNode = fixOrphanedListChild(listChildNode); 143 if (!listNode->hasTagName(listTag)) 144 // listChildNode will be removed from the list and a list of type m_type will be created. 145 switchListType = true; 146 Node* nextListChild; 147 Node* previousListChild; 148 VisiblePosition start; 149 VisiblePosition end; 150 if (listChildNode->hasTagName(liTag)) { 151 start = firstDeepEditingPositionForNode(listChildNode); 152 end = lastDeepEditingPositionForNode(listChildNode); 153 nextListChild = listChildNode->nextSibling(); 154 previousListChild = listChildNode->previousSibling(); 155 } else { 156 // A paragraph is visually a list item minus a list marker. The paragraph will be moved. 157 start = startOfParagraph(endingSelection().visibleStart()); 158 end = endOfParagraph(endingSelection().visibleEnd()); 159 nextListChild = enclosingListChild(end.next().deepEquivalent().node()); 160 ASSERT(nextListChild != listChildNode); 161 if (enclosingList(nextListChild) != listNode) 162 nextListChild = 0; 163 previousListChild = enclosingListChild(start.previous().deepEquivalent().node()); 164 ASSERT(previousListChild != listChildNode); 165 if (enclosingList(previousListChild) != listNode) 166 previousListChild = 0; 167 } 168 // When removing a list, we must always create a placeholder to act as a point of insertion 169 // for the list content being removed. 170 RefPtr<Element> placeholder = createBreakElement(document()); 171 RefPtr<Element> nodeToInsert = placeholder; 172 // If the content of the list item will be moved into another list, put it in a list item 173 // so that we don't create an orphaned list child. 174 if (enclosingList(listNode)) { 175 nodeToInsert = createListItemElement(document()); 176 appendNode(placeholder, nodeToInsert); 177 } 178 179 if (nextListChild && previousListChild) { 180 // We want to pull listChildNode out of listNode, and place it before nextListChild 181 // and after previousListChild, so we split listNode and insert it between the two lists. 182 // But to split listNode, we must first split ancestors of listChildNode between it and listNode, 183 // if any exist. 184 // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove 185 // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is 186 // unrendered. But we ought to remove nextListChild too, if it is unrendered. 187 splitElement(listNode, splitTreeToNode(nextListChild, listNode)); 188 insertNodeBefore(nodeToInsert, listNode); 189 } else if (nextListChild || listChildNode->parentNode() != listNode) { 190 // Just because listChildNode has no previousListChild doesn't mean there isn't any content 191 // in listNode that comes before listChildNode, as listChildNode could have ancestors 192 // between it and listNode. So, we split up to listNode before inserting the placeholder 193 // where we're about to move listChildNode to. 194 if (listChildNode->parentNode() != listNode) 195 splitElement(listNode, splitTreeToNode(listChildNode, listNode).get()); 196 insertNodeBefore(nodeToInsert, listNode); 197 } else 198 insertNodeAfter(nodeToInsert, listNode); 199 200 VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0)); 201 moveParagraphs(start, end, insertionPoint, true); 202 } 203 if (!listChildNode || switchListType || m_forceCreateList) { 204 // Create list. 205 VisiblePosition originalStart = endingSelection().visibleStart(); 206 VisiblePosition start = startOfParagraph(originalStart); 207 VisiblePosition end = endOfParagraph(endingSelection().visibleEnd()); 208 209 // Check for adjoining lists. 210 VisiblePosition previousPosition = start.previous(true); 211 VisiblePosition nextPosition = end.next(true); 212 RefPtr<HTMLElement> listItemElement = createListItemElement(document()); 213 RefPtr<HTMLElement> placeholder = createBreakElement(document()); 214 appendNode(placeholder, listItemElement); 215 Element* previousList = outermostEnclosingList(previousPosition.deepEquivalent().node()); 216 Element* nextList = outermostEnclosingList(nextPosition.deepEquivalent().node()); 217 Node* startNode = start.deepEquivalent().node(); 218 Node* previousCell = enclosingTableCell(previousPosition.deepEquivalent()); 219 Node* nextCell = enclosingTableCell(nextPosition.deepEquivalent()); 220 Node* currentCell = enclosingTableCell(start.deepEquivalent()); 221 if (previousList && (!previousList->hasTagName(listTag) || startNode->isDescendantOf(previousList) || previousCell != currentCell)) 222 previousList = 0; 223 if (nextList && (!nextList->hasTagName(listTag) || startNode->isDescendantOf(nextList) || nextCell != currentCell)) 224 nextList = 0; 225 // Place list item into adjoining lists. 226 if (previousList) 227 appendNode(listItemElement, previousList); 228 else if (nextList) 229 insertNodeAt(listItemElement, Position(nextList, 0)); 230 else { 231 // Create the list. 232 RefPtr<HTMLElement> listElement = m_type == OrderedList ? createOrderedListElement(document()) : createUnorderedListElement(document()); 233 m_listElement = listElement; 234 appendNode(listItemElement, listElement); 235 236 if (start == end && isBlock(start.deepEquivalent().node())) { 237 // Inserting the list into an empty paragraph that isn't held open 238 // by a br or a '\n', will invalidate start and end. Insert 239 // a placeholder and then recompute start and end. 240 RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent()); 241 start = VisiblePosition(Position(placeholder.get(), 0)); 242 end = start; 243 } 244 245 // Insert the list at a position visually equivalent to start of the 246 // paragraph that is being moved into the list. 247 // Try to avoid inserting it somewhere where it will be surrounded by 248 // inline ancestors of start, since it is easier for editing to produce 249 // clean markup when inline elements are pushed down as far as possible. 250 Position insertionPos(start.deepEquivalent().upstream()); 251 // Also avoid the containing list item. 252 Node* listChild = enclosingListChild(insertionPos.node()); 253 if (listChild && listChild->hasTagName(liTag)) 254 insertionPos = positionInParentBeforeNode(listChild); 255 256 insertNodeAt(listElement, insertionPos); 257 258 // We inserted the list at the start of the content we're about to move 259 // Update the start of content, so we don't try to move the list into itself. bug 19066 260 if (insertionPos == start.deepEquivalent()) 261 start = startOfParagraph(originalStart); 262 } 263 moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true); 264 if (nextList && previousList) 265 mergeIdenticalElements(previousList, nextList); 266 } 267 } 268 269 } 270