diff options
Diffstat (limited to 'Source/WebCore/editing/InsertListCommand.cpp')
-rw-r--r-- | Source/WebCore/editing/InsertListCommand.cpp | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/Source/WebCore/editing/InsertListCommand.cpp b/Source/WebCore/editing/InsertListCommand.cpp new file mode 100644 index 0000000..bb3cd93 --- /dev/null +++ b/Source/WebCore/editing/InsertListCommand.cpp @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2006, 2010 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "Element.h" +#include "InsertListCommand.h" +#include "DocumentFragment.h" +#include "htmlediting.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "TextIterator.h" +#include "visible_units.h" + +namespace WebCore { + +using namespace HTMLNames; + +static Node* enclosingListChild(Node* node, Node* listNode) +{ + Node* listChild = enclosingListChild(node); + while (listChild && enclosingList(listChild) != listNode) + listChild = enclosingListChild(listChild->parentNode()); + return listChild; +} + +PassRefPtr<HTMLElement> InsertListCommand::insertList(Document* document, Type type) +{ + RefPtr<InsertListCommand> insertCommand = create(document, type); + insertCommand->apply(); + return insertCommand->m_listElement; +} + +HTMLElement* InsertListCommand::fixOrphanedListChild(Node* node) +{ + RefPtr<HTMLElement> listElement = createUnorderedListElement(document()); + insertNodeBefore(listElement, node); + removeNode(node); + appendNode(node, listElement); + m_listElement = listElement; + return listElement.get(); +} + +PassRefPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtr<HTMLElement> passedList) +{ + RefPtr<HTMLElement> list = passedList; + Element* previousList = list->previousElementSibling(); + if (canMergeLists(previousList, list.get())) + mergeIdenticalElements(previousList, list); + + if (!list || !list->nextElementSibling() || !list->nextElementSibling()->isHTMLElement()) + return list.release(); + + RefPtr<HTMLElement> nextList = static_cast<HTMLElement*>(list->nextElementSibling()); + if (canMergeLists(list.get(), nextList.get())) { + mergeIdenticalElements(list, nextList); + return nextList.release(); + } + return list.release(); +} + +bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag) +{ + VisiblePosition start = selection.visibleStart(); + + if (!enclosingList(start.deepEquivalent().node())) + return false; + + VisiblePosition end = selection.visibleEnd(); + while (start.isNotNull() && start != end) { + Element* listNode = enclosingList(start.deepEquivalent().node()); + if (!listNode || !listNode->hasTagName(listTag)) + return false; + start = startOfNextParagraph(start); + } + + return true; +} + +InsertListCommand::InsertListCommand(Document* document, Type type) + : CompositeEditCommand(document), m_type(type) +{ +} + +void InsertListCommand::doApply() +{ + if (!endingSelection().isNonOrphanedCaretOrRange()) + return; + + if (!endingSelection().rootEditableElement()) + return; + + VisiblePosition visibleEnd = endingSelection().visibleEnd(); + VisiblePosition visibleStart = endingSelection().visibleStart(); + // When a selection ends at the start of a paragraph, we rarely paint + // the selection gap before that paragraph, because there often is no gap. + // In a case like this, it's not obvious to the user that the selection + // ends "inside" that paragraph, so it would be confusing if InsertUn{Ordered}List + // operated on that paragraph. + // FIXME: We paint the gap before some paragraphs that are indented with left + // margin/padding, but not others. We should make the gap painting more consistent and + // then use a left margin/padding rule here. + if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) + setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(true))); + + const QualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag; + if (endingSelection().isRange()) { + VisibleSelection selection = selectionForParagraphIteration(endingSelection()); + ASSERT(selection.isRange()); + VisiblePosition startOfSelection = selection.visibleStart(); + VisiblePosition endOfSelection = selection.visibleEnd(); + VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection); + + if (startOfParagraph(startOfSelection) != startOfLastParagraph) { + bool forceCreateList = !selectionHasListOfType(selection, listTag); + + RefPtr<Range> currentSelection = endingSelection().firstRange(); + VisiblePosition startOfCurrentParagraph = startOfSelection; + while (startOfCurrentParagraph != startOfLastParagraph) { + // doApply() may operate on and remove the last paragraph of the selection from the document + // if it's in the same list item as startOfCurrentParagraph. Return early to avoid an + // infinite loop and because there is no more work to be done. + // FIXME(<rdar://problem/5983974>): The endingSelection() may be incorrect here. Compute + // the new location of endOfSelection and use it as the end of the new selection. + if (!startOfLastParagraph.deepEquivalent().node()->inDocument()) + return; + setEndingSelection(startOfCurrentParagraph); + + // Save and restore endOfSelection and startOfLastParagraph when necessary + // since moveParagraph and movePragraphWithClones can remove nodes. + // FIXME: This is an inefficient way to keep selection alive because indexForVisiblePosition walks from + // the beginning of the document to the endOfSelection everytime this code is executed. + // But not using index is hard because there are so many ways we can lose selection inside doApplyForSingleParagraph. + int indexForEndOfSelection = indexForVisiblePosition(endOfSelection); + doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); + if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) { + RefPtr<Range> lastSelectionRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), indexForEndOfSelection, 0, true); + // If lastSelectionRange is null, then some contents have been deleted from the document. + // This should never happen and if it did, exit early immediately because we've lost the loop invariant. + ASSERT(lastSelectionRange); + if (!lastSelectionRange) + return; + endOfSelection = lastSelectionRange->startPosition(); + startOfLastParagraph = startOfParagraph(endOfSelection); + } + + // Fetch the start of the selection after moving the first paragraph, + // because moving the paragraph will invalidate the original start. + // We'll use the new start to restore the original selection after + // we modified all selected paragraphs. + if (startOfCurrentParagraph == startOfSelection) + startOfSelection = endingSelection().visibleStart(); + + startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart()); + } + setEndingSelection(endOfSelection); + doApplyForSingleParagraph(forceCreateList, listTag, currentSelection.get()); + // Fetch the end of the selection, for the reason mentioned above. + endOfSelection = endingSelection().visibleEnd(); + setEndingSelection(VisibleSelection(startOfSelection, endOfSelection)); + return; + } + } + + doApplyForSingleParagraph(false, listTag, endingSelection().firstRange().get()); +} + +void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const QualifiedName& listTag, Range* currentSelection) +{ + // FIXME: This will produce unexpected results for a selection that starts just before a + // table and ends inside the first cell, selectionForParagraphIteration should probably + // be renamed and deployed inside setEndingSelection(). + Node* selectionNode = endingSelection().start().node(); + Node* listChildNode = enclosingListChild(selectionNode); + bool switchListType = false; + if (listChildNode) { + // Remove the list chlild. + RefPtr<HTMLElement> listNode = enclosingList(listChildNode); + if (!listNode) { + listNode = fixOrphanedListChild(listChildNode); + listNode = mergeWithNeighboringLists(listNode); + } + if (!listNode->hasTagName(listTag)) + // listChildNode will be removed from the list and a list of type m_type will be created. + switchListType = true; + + // If the list is of the desired type, and we are not removing the list, then exit early. + if (!switchListType && forceCreateList) + return; + + // If the entire list is selected, then convert the whole list. + if (switchListType && isNodeVisiblyContainedWithin(listNode.get(), currentSelection)) { + bool rangeStartIsInList = visiblePositionBeforeNode(listNode.get()) == currentSelection->startPosition(); + bool rangeEndIsInList = visiblePositionAfterNode(listNode.get()) == currentSelection->endPosition(); + + RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag); + insertNodeBefore(newList, listNode); + + Node* firstChildInList = enclosingListChild(VisiblePosition(Position(listNode, 0)).deepEquivalent().node(), listNode.get()); + Node* outerBlock = firstChildInList->isBlockFlow() ? firstChildInList : listNode.get(); + + moveParagraphWithClones(firstPositionInNode(listNode.get()), lastPositionInNode(listNode.get()), newList.get(), outerBlock); + + // Manually remove listNode because moveParagraphWithClones sometimes leaves it behind in the document. + // See the bug 33668 and editing/execCommand/insert-list-orphaned-item-with-nested-lists.html. + // FIXME: This might be a bug in moveParagraphWithClones or deleteSelection. + if (listNode && listNode->inDocument()) + removeNode(listNode); + + newList = mergeWithNeighboringLists(newList); + + // Restore the start and the end of current selection if they started inside listNode + // because moveParagraphWithClones could have removed them. + ExceptionCode ec; + if (rangeStartIsInList && newList) + currentSelection->setStart(newList, 0, ec); + if (rangeEndIsInList && newList) + currentSelection->setEnd(newList, lastOffsetInNode(newList.get()), ec); + + setEndingSelection(VisiblePosition(firstPositionInNode(newList.get()))); + + return; + } + + unlistifyParagraph(endingSelection().visibleStart(), listNode.get(), listChildNode); + } + + if (!listChildNode || switchListType || forceCreateList) + m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag); +} + +void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listNode, Node* listChildNode) +{ + Node* nextListChild; + Node* previousListChild; + VisiblePosition start; + VisiblePosition end; + if (listChildNode->hasTagName(liTag)) { + start = firstDeepEditingPositionForNode(listChildNode); + end = lastDeepEditingPositionForNode(listChildNode); + nextListChild = listChildNode->nextSibling(); + previousListChild = listChildNode->previousSibling(); + } else { + // A paragraph is visually a list item minus a list marker. The paragraph will be moved. + start = startOfParagraph(originalStart); + end = endOfParagraph(start); + nextListChild = enclosingListChild(end.next().deepEquivalent().node(), listNode); + ASSERT(nextListChild != listChildNode); + previousListChild = enclosingListChild(start.previous().deepEquivalent().node(), listNode); + ASSERT(previousListChild != listChildNode); + } + // When removing a list, we must always create a placeholder to act as a point of insertion + // for the list content being removed. + RefPtr<Element> placeholder = createBreakElement(document()); + RefPtr<Element> nodeToInsert = placeholder; + // If the content of the list item will be moved into another list, put it in a list item + // so that we don't create an orphaned list child. + if (enclosingList(listNode)) { + nodeToInsert = createListItemElement(document()); + appendNode(placeholder, nodeToInsert); + } + + if (nextListChild && previousListChild) { + // We want to pull listChildNode out of listNode, and place it before nextListChild + // and after previousListChild, so we split listNode and insert it between the two lists. + // But to split listNode, we must first split ancestors of listChildNode between it and listNode, + // if any exist. + // FIXME: We appear to split at nextListChild as opposed to listChildNode so that when we remove + // listChildNode below in moveParagraphs, previousListChild will be removed along with it if it is + // unrendered. But we ought to remove nextListChild too, if it is unrendered. + splitElement(listNode, splitTreeToNode(nextListChild, listNode)); + insertNodeBefore(nodeToInsert, listNode); + } else if (nextListChild || listChildNode->parentNode() != listNode) { + // Just because listChildNode has no previousListChild doesn't mean there isn't any content + // in listNode that comes before listChildNode, as listChildNode could have ancestors + // between it and listNode. So, we split up to listNode before inserting the placeholder + // where we're about to move listChildNode to. + if (listChildNode->parentNode() != listNode) + splitElement(listNode, splitTreeToNode(listChildNode, listNode).get()); + insertNodeBefore(nodeToInsert, listNode); + } else + insertNodeAfter(nodeToInsert, listNode); + + VisiblePosition insertionPoint = VisiblePosition(Position(placeholder.get(), 0)); + moveParagraphs(start, end, insertionPoint, true); +} + +static Element* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag) +{ + Element* listNode = outermostEnclosingList(adjacentPos.deepEquivalent().node()); + + if (!listNode) + return 0; + + Node* previousCell = enclosingTableCell(pos.deepEquivalent()); + Node* currentCell = enclosingTableCell(adjacentPos.deepEquivalent()); + + if (!listNode->hasTagName(listTag) + || listNode->contains(pos.deepEquivalent().node()) + || previousCell != currentCell + || enclosingList(listNode) != enclosingList(pos.deepEquivalent().node())) + return 0; + + return listNode; +} + +PassRefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag) +{ + VisiblePosition start = startOfParagraph(originalStart); + VisiblePosition end = endOfParagraph(start); + + if (start.isNull() || end.isNull()) + return 0; + + // Check for adjoining lists. + RefPtr<HTMLElement> listItemElement = createListItemElement(document()); + RefPtr<HTMLElement> placeholder = createBreakElement(document()); + appendNode(placeholder, listItemElement); + + // Place list item into adjoining lists. + Element* previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(true), listTag); + Element* nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(true), listTag); + RefPtr<HTMLElement> listElement; + if (previousList) + appendNode(listItemElement, previousList); + else if (nextList) + insertNodeAt(listItemElement, Position(nextList, 0)); + else { + // Create the list. + listElement = createHTMLElement(document(), listTag); + appendNode(listItemElement, listElement); + + if (start == end && isBlock(start.deepEquivalent().node())) { + // Inserting the list into an empty paragraph that isn't held open + // by a br or a '\n', will invalidate start and end. Insert + // a placeholder and then recompute start and end. + RefPtr<Node> placeholder = insertBlockPlaceholder(start.deepEquivalent()); + start = VisiblePosition(Position(placeholder.get(), 0)); + end = start; + } + + // Insert the list at a position visually equivalent to start of the + // paragraph that is being moved into the list. + // Try to avoid inserting it somewhere where it will be surrounded by + // inline ancestors of start, since it is easier for editing to produce + // clean markup when inline elements are pushed down as far as possible. + Position insertionPos(start.deepEquivalent().upstream()); + // Also avoid the containing list item. + Node* listChild = enclosingListChild(insertionPos.node()); + if (listChild && listChild->hasTagName(liTag)) + insertionPos = positionInParentBeforeNode(listChild); + + insertNodeAt(listElement, insertionPos); + + // We inserted the list at the start of the content we're about to move + // Update the start of content, so we don't try to move the list into itself. bug 19066 + if (insertionPos == start.deepEquivalent()) + start = startOfParagraph(originalStart); + } + + moveParagraph(start, end, VisiblePosition(Position(placeholder.get(), 0)), true); + + if (listElement) + return mergeWithNeighboringLists(listElement); + + if (canMergeLists(previousList, nextList)) + mergeIdenticalElements(previousList, nextList); + + return listElement; +} + +} |