435 lines
21 KiB
C++
435 lines
21 KiB
C++
/*
|
|
* 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 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 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 "InsertListCommand.h"
|
|
|
|
#include "Editing.h"
|
|
#include "ElementTraversal.h"
|
|
#include "HTMLBRElement.h"
|
|
#include "HTMLLIElement.h"
|
|
#include "HTMLNames.h"
|
|
#include "HTMLUListElement.h"
|
|
#include "Range.h"
|
|
#include "VisibleUnits.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;
|
|
}
|
|
|
|
RefPtr<HTMLElement> InsertListCommand::insertList(Document& document, Type type)
|
|
{
|
|
RefPtr<InsertListCommand> insertCommand = create(document, type);
|
|
insertCommand->apply();
|
|
return insertCommand->m_listElement;
|
|
}
|
|
|
|
HTMLElement* InsertListCommand::fixOrphanedListChild(Node& node)
|
|
{
|
|
auto parentNode = makeRefPtr(node.parentNode());
|
|
if (parentNode && !parentNode->hasRichlyEditableStyle())
|
|
return nullptr;
|
|
|
|
auto listElement = HTMLUListElement::create(document());
|
|
insertNodeBefore(listElement.copyRef(), node);
|
|
if (!listElement->hasEditableStyle())
|
|
return nullptr;
|
|
|
|
removeNode(node);
|
|
appendNode(node, listElement.copyRef());
|
|
m_listElement = WTFMove(listElement);
|
|
return m_listElement.get();
|
|
}
|
|
|
|
Ref<HTMLElement> InsertListCommand::mergeWithNeighboringLists(HTMLElement& list)
|
|
{
|
|
Ref<HTMLElement> protectedList = list;
|
|
RefPtr previousList = list.previousElementSibling();
|
|
if (canMergeLists(previousList.get(), &list))
|
|
mergeIdenticalElements(*previousList, list);
|
|
|
|
RefPtr sibling = ElementTraversal::nextSibling(list);
|
|
if (!is<HTMLElement>(sibling))
|
|
return protectedList;
|
|
|
|
Ref<HTMLElement> nextList = downcast<HTMLElement>(*sibling);
|
|
if (canMergeLists(&list, nextList.ptr())) {
|
|
mergeIdenticalElements(list, nextList);
|
|
return nextList;
|
|
}
|
|
return protectedList;
|
|
}
|
|
|
|
bool InsertListCommand::selectionHasListOfType(const VisibleSelection& selection, const QualifiedName& listTag)
|
|
{
|
|
VisiblePosition start = selection.visibleStart();
|
|
|
|
if (!enclosingList(start.deepEquivalent().deprecatedNode()))
|
|
return false;
|
|
|
|
VisiblePosition end = startOfParagraph(selection.visibleEnd());
|
|
while (start.isNotNull() && start != end) {
|
|
Element* listNode = enclosingList(start.deepEquivalent().deprecatedNode());
|
|
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()
|
|
{
|
|
VisiblePosition visibleEnd = endingSelection().visibleEnd();
|
|
VisiblePosition visibleStart = endingSelection().visibleStart();
|
|
|
|
if (visibleEnd.isNull() || visibleStart.isNull() || !endingSelection().isContentRichlyEditable())
|
|
return;
|
|
|
|
// 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;
|
|
}
|
|
|
|
auto& listTag = (m_type == Type::OrderedList) ? olTag : ulTag;
|
|
if (endingSelection().isRange()) {
|
|
VisibleSelection selection = selectionForParagraphIteration(endingSelection());
|
|
if (selection.isRange()) {
|
|
VisiblePosition startOfSelection = selection.visibleStart();
|
|
VisiblePosition endOfSelection = selection.visibleEnd();
|
|
VisiblePosition startOfLastParagraph = startOfParagraph(endOfSelection, CanSkipOverEditingBoundary);
|
|
|
|
if (startOfLastParagraph.isNotNull() && startOfParagraph(startOfSelection, CanSkipOverEditingBoundary) != startOfLastParagraph) {
|
|
bool forceCreateList = !selectionHasListOfType(selection, listTag);
|
|
|
|
auto currentSelection = *endingSelection().firstRange();
|
|
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().anchorNode()->isConnected())
|
|
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.
|
|
RefPtr<ContainerNode> scope;
|
|
int indexForEndOfSelection = indexForVisiblePosition(endOfSelection, scope);
|
|
doApplyForSingleParagraph(forceCreateList, listTag, currentSelection);
|
|
if (endOfSelection.isNull() || endOfSelection.isOrphan() || startOfLastParagraph.isNull() || startOfLastParagraph.isOrphan()) {
|
|
endOfSelection = visiblePositionForIndex(indexForEndOfSelection, scope.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);
|
|
}
|
|
|
|
// 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);
|
|
// Fetch the end of the selection, for the reason mentioned above.
|
|
endOfSelection = endingSelection().visibleEnd();
|
|
setEndingSelection(VisibleSelection(startOfSelection, endOfSelection, endingSelection().isDirectional()));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
auto range = endingSelection().firstRange();
|
|
doApplyForSingleParagraph(false, listTag, *range);
|
|
}
|
|
|
|
EditAction InsertListCommand::editingAction() const
|
|
{
|
|
return m_type == Type::OrderedList ? EditAction::InsertOrderedList : EditAction::InsertUnorderedList;
|
|
}
|
|
|
|
void InsertListCommand::doApplyForSingleParagraph(bool forceCreateList, const HTMLQualifiedName& listTag, SimpleRange& 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 child.
|
|
RefPtr<HTMLElement> listNode = enclosingList(listChildNode);
|
|
if (!listNode) {
|
|
RefPtr<HTMLElement> listElement = fixOrphanedListChild(*listChildNode);
|
|
if (!listElement || !listElement->isConnected())
|
|
return;
|
|
|
|
listNode = mergeWithNeighboringLists(*listElement);
|
|
}
|
|
|
|
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, currentSelection)) {
|
|
bool rangeStartIsInList = visiblePositionBeforeNode(*listNode) == makeDeprecatedLegacyPosition(currentSelection.start);
|
|
bool rangeEndIsInList = visiblePositionAfterNode(*listNode) == makeDeprecatedLegacyPosition(currentSelection.end);
|
|
|
|
RefPtr<HTMLElement> newList = createHTMLElement(document(), listTag);
|
|
insertNodeBefore(*newList, *listNode);
|
|
if (!newList->hasEditableStyle())
|
|
return;
|
|
|
|
auto* firstChildInList = enclosingListChild(VisiblePosition(firstPositionInNode(listNode.get())).deepEquivalent().deprecatedNode(), listNode.get());
|
|
Node* outerBlock = firstChildInList && isBlockFlowElement(*firstChildInList) ? 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->isConnected())
|
|
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.
|
|
if (rangeStartIsInList && newList)
|
|
currentSelection.start = makeBoundaryPointBeforeNodeContents(*newList);
|
|
if (rangeEndIsInList && newList)
|
|
currentSelection.end = makeBoundaryPointAfterNodeContents(*newList);
|
|
|
|
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)
|
|
{
|
|
RefPtr<Node> nextListChild;
|
|
RefPtr<Node> previousListChild;
|
|
VisiblePosition start;
|
|
VisiblePosition end;
|
|
|
|
if (!listNode->parentNode()->hasEditableStyle())
|
|
return;
|
|
|
|
if (listChildNode->hasTagName(liTag)) {
|
|
start = firstPositionInNode(listChildNode);
|
|
end = 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(), listNode);
|
|
ASSERT(nextListChild != listChildNode);
|
|
previousListChild = enclosingListChild(start.previous().deepEquivalent().deprecatedNode(), listNode);
|
|
ASSERT(previousListChild != listChildNode);
|
|
}
|
|
|
|
if (start.isNull() || end.isNull())
|
|
return;
|
|
|
|
// When removing a list, we must always create a placeholder to act as a point of insertion
|
|
// for the list content being removed.
|
|
auto placeholder = HTMLBRElement::create(document());
|
|
RefPtr<Element> nodeToInsert = placeholder.copyRef();
|
|
// 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 = HTMLLIElement::create(document());
|
|
appendNode(placeholder.copyRef(), *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.releaseNonNull(), *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 (auto listChildNodeParentNode = makeRefPtr(listChildNode->parentNode()); listChildNodeParentNode && listChildNodeParentNode != listNode)
|
|
splitElement(*listNode, *splitTreeToNode(*listChildNode, *listNode).get());
|
|
insertNodeBefore(nodeToInsert.releaseNonNull(), *listNode);
|
|
} else
|
|
insertNodeAfter(nodeToInsert.releaseNonNull(), *listNode);
|
|
|
|
VisiblePosition insertionPoint = VisiblePosition(positionBeforeNode(placeholder.ptr()));
|
|
moveParagraphs(start, end, insertionPoint, true);
|
|
}
|
|
|
|
static RefPtr<Element> adjacentEnclosingList(const VisiblePosition& pos, const VisiblePosition& adjacentPos, const QualifiedName& listTag)
|
|
{
|
|
RefPtr<Element> listNode = outermostEnclosingList(adjacentPos.deepEquivalent().deprecatedNode());
|
|
|
|
if (!listNode)
|
|
return nullptr;
|
|
|
|
RefPtr previousCell = enclosingTableCell(pos.deepEquivalent());
|
|
RefPtr currentCell = enclosingTableCell(adjacentPos.deepEquivalent());
|
|
|
|
if (!listNode->hasTagName(listTag)
|
|
|| listNode->contains(pos.deepEquivalent().deprecatedNode())
|
|
|| previousCell != currentCell
|
|
|| enclosingList(listNode.get()) != enclosingList(pos.deepEquivalent().deprecatedNode()))
|
|
return nullptr;
|
|
|
|
return listNode;
|
|
}
|
|
|
|
RefPtr<HTMLElement> InsertListCommand::listifyParagraph(const VisiblePosition& originalStart, const QualifiedName& listTag)
|
|
{
|
|
VisiblePosition start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
|
|
VisiblePosition end = endOfParagraph(start, CanSkipOverEditingBoundary);
|
|
|
|
if (start.isNull() || end.isNull() || !start.deepEquivalent().containerNode()->hasEditableStyle() || !end.deepEquivalent().containerNode()->hasEditableStyle())
|
|
return 0;
|
|
|
|
// Check for adjoining lists.
|
|
auto listItemElement = HTMLLIElement::create(document());
|
|
auto placeholder = HTMLBRElement::create(document());
|
|
appendNode(placeholder.copyRef(), listItemElement.copyRef());
|
|
|
|
// Place list item into adjoining lists.
|
|
auto previousList = adjacentEnclosingList(start.deepEquivalent(), start.previous(CannotCrossEditingBoundary), listTag);
|
|
auto nextList = adjacentEnclosingList(start.deepEquivalent(), end.next(CannotCrossEditingBoundary), listTag);
|
|
RefPtr<HTMLElement> listElement;
|
|
if (previousList)
|
|
appendNode(WTFMove(listItemElement), *previousList);
|
|
else if (nextList)
|
|
insertNodeAt(WTFMove(listItemElement), positionBeforeNode(nextList.get()));
|
|
else {
|
|
// Create the list.
|
|
listElement = createHTMLElement(document(), listTag);
|
|
appendNode(WTFMove(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.
|
|
auto blockPlaceholder = insertBlockPlaceholder(start.deepEquivalent());
|
|
start = positionBeforeNode(blockPlaceholder.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.
|
|
RefPtr listChild = enclosingListChild(insertionPos.deprecatedNode());
|
|
if (listChild && listChild->hasTagName(liTag))
|
|
insertionPos = positionInParentBeforeNode(listChild.get());
|
|
|
|
if (!isEditablePosition(insertionPos))
|
|
return 0;
|
|
|
|
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()) {
|
|
listElement->document().updateLayoutIgnorePendingStylesheets();
|
|
start = startOfParagraph(originalStart, CanSkipOverEditingBoundary);
|
|
end = endOfParagraph(start, CanSkipOverEditingBoundary);
|
|
}
|
|
}
|
|
|
|
moveParagraph(start, end, positionBeforeNode(placeholder.ptr()), true);
|
|
|
|
if (listElement)
|
|
return mergeWithNeighboringLists(*listElement);
|
|
|
|
if (canMergeLists(previousList.get(), nextList.get()))
|
|
mergeIdenticalElements(*previousList, *nextList);
|
|
|
|
return listElement;
|
|
}
|
|
|
|
}
|