blob: 27b76a50d27520c7ebe0d7653c499bc9a92ccdf9 [file] [log] [blame]
/*
* 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 "core/editing/InsertListCommand.h"
#include "bindings/core/v8/ExceptionStatePlaceholder.h"
#include "core/HTMLNames.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementTraversal.h"
#include "core/editing/TextIterator.h"
#include "core/editing/VisibleUnits.h"
#include "core/editing/htmlediting.h"
#include "core/html/HTMLBRElement.h"
#include "core/html/HTMLElement.h"
#include "core/html/HTMLLIElement.h"
#include "core/html/HTMLUListElement.h"
namespace blink {
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;
}
HTMLUListElement* InsertListCommand::fixOrphanedListChild(Node* node)
{
RefPtrWillBeRawPtr<HTMLUListElement> listElement = createUnorderedListElement(document());
insertNodeBefore(listElement, node);
removeNode(node);
appendNode(node, listElement);
m_listElement = listElement;
return listElement.get();
}
PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::mergeWithNeighboringLists(PassRefPtrWillBeRawPtr<HTMLElement> passedList)
{
RefPtrWillBeRawPtr<HTMLElement> list = passedList;
Element* previousList = ElementTraversal::previousSibling(*list);
if (canMergeLists(previousList, list.get()))
mergeIdenticalElements(previousList, list);
if (!list)
return nullptr;
Element* nextSibling = ElementTraversal::nextSibling(*list);
if (!nextSibling || !nextSibling->isHTMLElement())
return list.release();
RefPtrWillBeRawPtr<HTMLElement> nextList = toHTMLElement(nextSibling);
if (canMergeLists(list.get(), nextList.get())) {
mergeIdenticalElements(list, nextList);
return nextList.release();
}
return list.release();
}
bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const HTMLQualifiedName& listTag)
{
VisiblePosition start = selection.visibleStart();
if (!enclosingList(start.deepEquivalent().deprecatedNode()))
return false;
VisiblePosition end = startOfParagraph(selection.visibleEnd());
while (start.isNotNull() && start != end) {
HTMLElement* listElement = enclosingList(start.deepEquivalent().deprecatedNode());
if (!listElement || !listElement->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, CanSkipOverEditingBoundary)) {
setEndingSelection(VisibleSelection(visibleStart, visibleEnd.previous(CannotCrossEditingBoundary), endingSelection().isDirectional()));
if (!endingSelection().rootEditableElement())
return;
}
const HTMLQualifiedName& listTag = (m_type == OrderedList) ? olTag : ulTag;
if (endingSelection().isRange()) {
bool forceListCreation = false;
VisibleSelection selection = selectionForParagraphIteration(endingSelection());
ASSERT(selection.isRange());
VisiblePosition startOfSelection = selection.visibleStart();
VisiblePosition endOfSelection = selection.visibleEnd();
VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
RefPtrWillBeRawPtr<Range> currentSelection = endingSelection().firstRange();
RefPtrWillBeRawPtr<ContainerNode> scopeForStartOfSelection = nullptr;
RefPtrWillBeRawPtr<ContainerNode> scopeForEndOfSelection = nullptr;
// 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 los eselection inside doApplyForSingleParagraph.
int indexForStartOfSelection = indexForVisiblePosition(startOfSelection, scopeForStartOfSelection);
int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scopeForEndOfSelection);
if (startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) {
forceListCreation = !selectionHasListOfType(selection, listTag);
VisiblePosition startOfCurrentParagraph = startOfSelection;
while (startOfCurrentParagraph.isNotNull() && !inSameParagraph(startOfCurrentParagraph, startOfLastParagraph, CanCrossEditingBoundary)) {
// 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().inDocument())
return;
setEndingSelection(startOfCurrentParagraph);
// Save and restore endOfSelection and startOfLastParagraph when necessary
// since moveParagraph and movePragraphWithClones can remove nodes.
doApplyForSingleParagraph(forceListCreation, listTag, *currentSelection);
if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) {
endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scopeForEndOfSelection.get());
// If endOfSelection 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(endOfSelection.isNotNull());
if (endOfSelection.isNull())
return;
startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
}
startOfCurrentParagraph = startOfNextParagraph(endingSelection().visibleStart());
}
setEndingSelection(endOfSelection);
}
doApplyForSingleParagraph(forceListCreation, listTag, *currentSelection);
// Fetch the end of the selection, for the reason mentioned above.
if (endOfSelection.isNull() || endOfSelection.isOrphan()) {
endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scopeForEndOfSelection.get());
if (endOfSelection.isNull())
return;
}
if (startOfSelection.isNull() || startOfSelection.isOrphan()) {
startOfSelection = visiblePositionForIndex(indexForStartOfSelection, scopeForStartOfSelection.get());
if (startOfSelection.isNull())
return;
}
setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional()));
return;
}
ASSERT(endingSelection().firstRange());
doApplyForSingleParagraph(false, listTag, *endingSelection().firstRange());
}
void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const HTMLQualifiedName& 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().deprecatedNode();
Node* listChildNode = enclosingListChild(selectionNode);
bool switchListType = false;
if (listChildNode) {
// Remove the list chlild.
RefPtrWillBeRawPtr<HTMLElement> listElement = enclosingList(listChildNode);
if (!listElement) {
listElement = fixOrphanedListChild(listChildNode);
listElement = mergeWithNeighboringLists(listElement);
}
if (!listElement->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(*listElement, currentSelection)) {
bool rangeStartIsInList = visiblePositionBeforeNode(*listElement) == VisiblePosition(currentSelection.startPosition());
bool rangeEndIsInList = visiblePositionAfterNode(*listElement) == VisiblePosition(currentSelection.endPosition());
RefPtrWillBeRawPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
insertNodeBefore(newList, listElement);
Node* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listElement.get())).deepEquivalent().deprecatedNode(), listElement.get());
Element* outerBlock = firstChildInList && isBlockFlowElement(*firstChildInList) ? toElement(firstChildInList) : listElement.get();
moveParagraphWithClones(VisiblePosition(firstPositionInNode(listElement.get())), VisiblePosition(lastPositionInNode(listElement.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 (listElement && listElement->inDocument())
removeNode(listElement);
newList = mergeWithNeighboringLists(newList);
// Restore the start and the end of current selection if they started inside listNode
// because moveParagraphWithClones could have removed them.
if (rangeStartIsInList && newList)
currentSelection.setStart(newList, 0, IGNORE_EXCEPTION);
if (rangeEndIsInList && newList)
currentSelection.setEnd(newList, lastOffsetInNode(newList.get()), IGNORE_EXCEPTION);
setEndingSelection(VisiblePosition(firstPositionInNode(newList.get())));
return;
}
unlistifyParagraph(endingSelection().visibleStart(), listElement.get(), listChildNode);
}
if (!listChildNode || switchListType || forceCreateList)
m_listElement = listifyParagraph(endingSelection().visibleStart(), listTag);
}
void InsertListCommand::unlistifyParagraph(const VisiblePosition& originalStart, HTMLElement* listElement, Node* listChildNode)
{
Node* nextListChild;
Node* previousListChild;
VisiblePosition start;
VisiblePosition end;
ASSERT(listChildNode);
if (isHTMLLIElement(*listChildNode)) {
start = VisiblePosition(firstPositionInNode(listChildNode));
end = VisiblePosition(lastPositionInNode(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, CanSkipOverEditingBoundary);
end = endOfParagraph(start, CanSkipOverEditingBoundary);
nextListChild = enclosingListChild(end.next().deepEquivalent().deprecatedNode(), listElement);
ASSERT(nextListChild != listChildNode);
previousListChild = enclosingListChild(start.previous().deepEquivalent().deprecatedNode(), listElement);
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.
RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document());
RefPtrWillBeRawPtr<HTMLElement> elementToInsert = 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(listElement)) {
elementToInsert = createListItemElement(document());
appendNode(placeholder, elementToInsert);
}
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(listElement, splitTreeToNode(nextListChild, listElement));
insertNodeBefore(elementToInsert, listElement);
} else if (nextListChild || listChildNode->parentNode() != listElement) {
// 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() != listElement)
splitElement(listElement, splitTreeToNode(listChildNode, listElement).get());
insertNodeBefore(elementToInsert, listElement);
} else {
insertNodeAfter(elementToInsert, listElement);
}
VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.get()));
moveParagraphs(start, end, insertionPoint, /* preserveSelection */ true, /* preserveStyle */ true, listChildNode);
}
static HTMLElement* adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const HTMLQualifiedName& listTag)
{
HTMLElement* listElement = outermostEnclosingList(adjacentPos.deepEquivalent().deprecatedNode());
if (!listElement)
return 0;
Element* previousCell = enclosingTableCell(pos.deepEquivalent());
Element* currentCell = enclosingTableCell(adjacentPos.deepEquivalent());
if (!listElement->hasTagName(listTag)
|| listElement->contains(pos.deepEquivalent().deprecatedNode())
|| previousCell != currentCell
|| enclosingList(listElement) != enclosingList(pos.deepEquivalent().deprecatedNode()))
return 0;
return listElement;
}
PassRefPtrWillBeRawPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const HTMLQualifiedName& listTag)
{
VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary);
if (start.isNull() || end.isNull())
return nullptr;
// Check for adjoining lists.
RefPtrWillBeRawPtr<HTMLElement> listItemElement = createListItemElement(document());
RefPtrWillBeRawPtr<HTMLBRElement> placeholder = createBreakElement(document());
appendNode(placeholder, listItemElement);
// Place list item into adjoining lists.
HTMLElement* previousList = adjacentEnclosingList(start, start.previous(CannotCrossEditingBoundary), listTag);
HTMLElement* nextList = adjacentEnclosingList(start, end.next(CannotCrossEditingBoundary), listTag);
RefPtrWillBeRawPtr<HTMLElement> listElement = nullptr;
if (previousList)
appendNode(listItemElement, previousList);
else if (nextList)
insertNodeAt(listItemElement, positionBeforeNode(nextList));
else {
// Create the list.
listElement = createHTMLElement(document(), listTag);
appendNode(listItemElement, listElement);
if (start == end && isBlock(start.deepEquivalent().deprecatedNode())) {
// 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.
RefPtrWillBeRawPtr<HTMLBRElement> placeholder = insertBlockPlaceholder(start.deepEquivalent());
start = VisiblePosition(positionBeforeNode(placeholder.get()));
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.deprecatedNode());
if (isHTMLLIElement(listChild))
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
// Layout is necessary since start's node's inline renderers may have been destroyed by the insertion
// The end of the content may have changed after the insertion and layout so update it as well.
if (insertionPos == start.deepEquivalent())
start = originalStart;
}
// Inserting list element and list item list may change start of pargraph
// to move. We calculate start of paragraph again.
document().updateLayoutIgnorePendingStylesheets();
start = startOfParagraph(start, CanSkipOverEditingBoundary);
end = endOfParagraph(start, CanSkipOverEditingBoundary);
moveParagraph(start, end, VisiblePosition(positionBeforeNode(placeholder.get())), true);
if (listElement)
return mergeWithNeighboringLists(listElement);
if (canMergeLists(previousList, nextList))
mergeIdenticalElements(previousList, nextList);
return listElement;
}
void InsertListCommand::trace(Visitor* visitor)
{
visitor->trace(m_listElement);
CompositeEditCommand::trace(visitor);
}
}