mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
The primary goal of this change was to remove EventTarget from the
sky_engine C++ code. Since EventTarget is so core to the entire event
system that sky_engine was based on, this is a rather invasive change.
As such, it had some knock-on effects. I deleted some of the files
that were affected, and cauterised the remainder.
In many cases, a file would depend on another file that it didn't
include directly, but instead included indirectly via another file
that I deleted. When this happened, if the features that this broke
were obsolete, I sometimes just removed the features instead.
Specifically:
- removed EventTarget
- removed EventQueue, since without a target, what's a queue going to
do?
- same with EventDispatch*
- removed ExecutionContext, since it had an EventQueue and nothing
else it did was relevant to Sky anymore
- removed ActiveDOMObject, which was all about ExecutionContexts
- removed ContextLifecycleNotifier since it dependend on
ExecutionContext and ActiveDOMObject
- removed the other Lifecycle classes for consistency, and replaced
them with four booleans in the Document class
- removed some of the attributes that are no longer relevant from
IDLExtendedAttributes (ConstructorCallWith and
CallWith=ExecutionContext)
- removed the Document member on DOMDartState since we never set it to
anything but null.
- removed BuiltinSky::InstallWindow since it relied on the Document
member of DOMDartState
- removed EventHandler, EventListener, and mentions of those in
various binding scripts
- removed NewEventHandler, since we're not using that either
- removed the following interfaces from the Sky Dart API:
- EventTarget
- EventListener (since without a target, there's no way to listen)
- FocusEvent (since it's only member was an EventTarget)
- HashChangeEvent (mostly by accident, but it's defunct anyway)
- FontFace (it used ConstructorCallWith=ExecutionContext)
- changed the following interfaces of the Sky DART API:
- MediaQueryList is no longer an EventTarget
- Node is no longer an EventTarget
- Document no longer has defaultView (depended on
DOMDartState's document)
- DocumentFragment, Element, Range, and Text no longer have a
constructor (they all depended on DOMDartState's document, which
is now gone)
- Event lost its EventTarget members and path.
- Window lost its WindowTimers partial interface (it used
EventTarget and ExecutionContext a lot)
- removed numerous hacks in the bindings around features that are now
gone, like addEventListener
- removed a bunch of console logging code, since that relied on
ExecutionContext
- cauterised the wound in FontFace.cpp by removing constructors and
methods that called now-removed features
- same with MediaQuery and friends
- same with some editor features and focus-related features
- same with Document
- removed DOMTimer classes since they use ExecutionContexts
857 lines
33 KiB
C++
857 lines
33 KiB
C++
/*
|
|
* Copyright (C) 2005, 2006, 2007, 2008 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 "sky/engine/core/editing/CompositeEditCommand.h"
|
|
|
|
#include "sky/engine/bindings/exception_state_placeholder.h"
|
|
#include "sky/engine/core/dom/Document.h"
|
|
#include "sky/engine/core/dom/DocumentFragment.h"
|
|
#include "sky/engine/core/dom/DocumentMarkerController.h"
|
|
#include "sky/engine/core/dom/ElementTraversal.h"
|
|
#include "sky/engine/core/dom/NodeTraversal.h"
|
|
#include "sky/engine/core/dom/Range.h"
|
|
#include "sky/engine/core/dom/Text.h"
|
|
#include "sky/engine/core/editing/AppendNodeCommand.h"
|
|
#include "sky/engine/core/editing/DeleteFromTextNodeCommand.h"
|
|
#include "sky/engine/core/editing/DeleteSelectionCommand.h"
|
|
#include "sky/engine/core/editing/Editor.h"
|
|
#include "sky/engine/core/editing/InsertIntoTextNodeCommand.h"
|
|
#include "sky/engine/core/editing/InsertLineBreakCommand.h"
|
|
#include "sky/engine/core/editing/InsertNodeBeforeCommand.h"
|
|
#include "sky/engine/core/editing/InsertParagraphSeparatorCommand.h"
|
|
#include "sky/engine/core/editing/PlainTextRange.h"
|
|
#include "sky/engine/core/editing/RemoveNodeCommand.h"
|
|
#include "sky/engine/core/editing/RemoveNodePreservingChildrenCommand.h"
|
|
#include "sky/engine/core/editing/ReplaceSelectionCommand.h"
|
|
#include "sky/engine/core/editing/SpellChecker.h"
|
|
#include "sky/engine/core/editing/SplitElementCommand.h"
|
|
#include "sky/engine/core/editing/SplitTextNodeCommand.h"
|
|
#include "sky/engine/core/editing/TextIterator.h"
|
|
#include "sky/engine/core/editing/VisibleUnits.h"
|
|
#include "sky/engine/core/editing/htmlediting.h"
|
|
#include "sky/engine/core/frame/LocalFrame.h"
|
|
#include "sky/engine/core/html/HTMLElement.h"
|
|
#include "sky/engine/core/rendering/InlineTextBox.h"
|
|
#include "sky/engine/core/rendering/RenderBlock.h"
|
|
#include "sky/engine/core/rendering/RenderText.h"
|
|
|
|
namespace blink {
|
|
|
|
PassRefPtr<EditCommandComposition> EditCommandComposition::create(Document* document,
|
|
const VisibleSelection& startingSelection, const VisibleSelection& endingSelection, EditAction editAction)
|
|
{
|
|
return adoptRef(new EditCommandComposition(document, startingSelection, endingSelection, editAction));
|
|
}
|
|
|
|
EditCommandComposition::EditCommandComposition(Document* document, const VisibleSelection& startingSelection, const VisibleSelection& endingSelection, EditAction editAction)
|
|
: m_document(document)
|
|
, m_startingSelection(startingSelection)
|
|
, m_endingSelection(endingSelection)
|
|
, m_startingRootEditableElement(startingSelection.rootEditableElement())
|
|
, m_endingRootEditableElement(endingSelection.rootEditableElement())
|
|
, m_editAction(editAction)
|
|
{
|
|
}
|
|
|
|
bool EditCommandComposition::belongsTo(const LocalFrame& frame) const
|
|
{
|
|
ASSERT(m_document);
|
|
return m_document->frame() == &frame;
|
|
}
|
|
|
|
void EditCommandComposition::unapply()
|
|
{
|
|
ASSERT(m_document);
|
|
RefPtr<LocalFrame> frame = m_document->frame();
|
|
ASSERT(frame);
|
|
|
|
// Changes to the document may have been made since the last editing operation that require a layout, as in <rdar://problem/5658603>.
|
|
// Low level operations, like RemoveNodeCommand, don't require a layout because the high level operations that use them perform one
|
|
// if one is necessary (like for the creation of VisiblePositions).
|
|
m_document->updateLayout();
|
|
|
|
{
|
|
size_t size = m_commands.size();
|
|
for (size_t i = size; i; --i)
|
|
m_commands[i - 1]->doUnapply();
|
|
}
|
|
|
|
frame->editor().unappliedEditing(this);
|
|
}
|
|
|
|
void EditCommandComposition::reapply()
|
|
{
|
|
ASSERT(m_document);
|
|
RefPtr<LocalFrame> frame = m_document->frame();
|
|
ASSERT(frame);
|
|
|
|
// Changes to the document may have been made since the last editing operation that require a layout, as in <rdar://problem/5658603>.
|
|
// Low level operations, like RemoveNodeCommand, don't require a layout because the high level operations that use them perform one
|
|
// if one is necessary (like for the creation of VisiblePositions).
|
|
m_document->updateLayout();
|
|
|
|
{
|
|
size_t size = m_commands.size();
|
|
for (size_t i = 0; i != size; ++i)
|
|
m_commands[i]->doReapply();
|
|
}
|
|
|
|
frame->editor().reappliedEditing(this);
|
|
}
|
|
|
|
void EditCommandComposition::append(SimpleEditCommand* command)
|
|
{
|
|
m_commands.append(command);
|
|
}
|
|
|
|
void EditCommandComposition::setStartingSelection(const VisibleSelection& selection)
|
|
{
|
|
m_startingSelection = selection;
|
|
m_startingRootEditableElement = selection.rootEditableElement();
|
|
}
|
|
|
|
void EditCommandComposition::setEndingSelection(const VisibleSelection& selection)
|
|
{
|
|
m_endingSelection = selection;
|
|
m_endingRootEditableElement = selection.rootEditableElement();
|
|
}
|
|
|
|
CompositeEditCommand::CompositeEditCommand(Document& document)
|
|
: EditCommand(document)
|
|
{
|
|
}
|
|
|
|
CompositeEditCommand::~CompositeEditCommand()
|
|
{
|
|
ASSERT(isTopLevelCommand() || !m_composition);
|
|
}
|
|
|
|
void CompositeEditCommand::apply()
|
|
{
|
|
if (!endingSelection().isContentRichlyEditable()) {
|
|
switch (editingAction()) {
|
|
case EditActionTyping:
|
|
case EditActionPaste:
|
|
case EditActionDrag:
|
|
case EditActionSetWritingDirection:
|
|
case EditActionCut:
|
|
case EditActionUnspecified:
|
|
break;
|
|
default:
|
|
ASSERT_NOT_REACHED();
|
|
return;
|
|
}
|
|
}
|
|
ensureComposition();
|
|
|
|
// Changes to the document may have been made since the last editing operation that require a layout, as in <rdar://problem/5658603>.
|
|
// Low level operations, like RemoveNodeCommand, don't require a layout because the high level operations that use them perform one
|
|
// if one is necessary (like for the creation of VisiblePositions).
|
|
document().updateLayout();
|
|
|
|
LocalFrame* frame = document().frame();
|
|
ASSERT(frame);
|
|
{
|
|
doApply();
|
|
}
|
|
|
|
// Only need to call appliedEditing for top-level commands,
|
|
// and TypingCommands do it on their own (see TypingCommand::typingAddedToOpenCommand).
|
|
if (!isTypingCommand())
|
|
frame->editor().appliedEditing(this);
|
|
setShouldRetainAutocorrectionIndicator(false);
|
|
}
|
|
|
|
EditCommandComposition* CompositeEditCommand::ensureComposition()
|
|
{
|
|
CompositeEditCommand* command = this;
|
|
while (command && command->parent())
|
|
command = command->parent();
|
|
if (!command->m_composition)
|
|
command->m_composition = EditCommandComposition::create(&document(), startingSelection(), endingSelection(), editingAction());
|
|
return command->m_composition.get();
|
|
}
|
|
|
|
bool CompositeEditCommand::preservesTypingStyle() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool CompositeEditCommand::isTypingCommand() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void CompositeEditCommand::setShouldRetainAutocorrectionIndicator(bool)
|
|
{
|
|
}
|
|
|
|
//
|
|
// sugary-sweet convenience functions to help create and apply edit commands in composite commands
|
|
//
|
|
void CompositeEditCommand::applyCommandToComposite(PassRefPtr<EditCommand> prpCommand)
|
|
{
|
|
RefPtr<EditCommand> command = prpCommand;
|
|
command->setParent(this);
|
|
command->doApply();
|
|
if (command->isSimpleEditCommand()) {
|
|
command->setParent(0);
|
|
ensureComposition()->append(toSimpleEditCommand(command.get()));
|
|
}
|
|
m_commands.append(command.release());
|
|
}
|
|
|
|
void CompositeEditCommand::applyCommandToComposite(PassRefPtr<CompositeEditCommand> command, const VisibleSelection& selection)
|
|
{
|
|
command->setParent(this);
|
|
if (selection != command->endingSelection()) {
|
|
command->setStartingSelection(selection);
|
|
command->setEndingSelection(selection);
|
|
}
|
|
command->doApply();
|
|
m_commands.append(command);
|
|
}
|
|
|
|
void CompositeEditCommand::insertParagraphSeparator(bool useDefaultParagraphElement, bool pasteBlockqutoeIntoUnquotedArea)
|
|
{
|
|
applyCommandToComposite(InsertParagraphSeparatorCommand::create(document(), useDefaultParagraphElement, pasteBlockqutoeIntoUnquotedArea));
|
|
}
|
|
|
|
bool CompositeEditCommand::isRemovableBlock(const Node* node)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void CompositeEditCommand::insertNodeBefore(PassRefPtr<Node> insertChild, PassRefPtr<Node> refChild, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable)
|
|
{
|
|
applyCommandToComposite(InsertNodeBeforeCommand::create(insertChild, refChild, shouldAssumeContentIsAlwaysEditable));
|
|
}
|
|
|
|
void CompositeEditCommand::insertNodeAfter(PassRefPtr<Node> insertChild, PassRefPtr<Node> refChild)
|
|
{
|
|
ASSERT(insertChild);
|
|
ASSERT(refChild);
|
|
ContainerNode* parent = refChild->parentNode();
|
|
ASSERT(parent);
|
|
ASSERT(!parent->isShadowRoot());
|
|
if (parent->lastChild() == refChild)
|
|
appendNode(insertChild, parent);
|
|
else {
|
|
ASSERT(refChild->nextSibling());
|
|
insertNodeBefore(insertChild, refChild->nextSibling());
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommand::insertNodeAt(PassRefPtr<Node> insertChild, const Position& editingPosition)
|
|
{
|
|
ASSERT(isEditablePosition(editingPosition, ContentIsEditable, DoNotUpdateStyle));
|
|
// For editing positions like [table, 0], insert before the table,
|
|
// likewise for replaced elements, brs, etc.
|
|
Position p = editingPosition.parentAnchoredEquivalent();
|
|
Node* refChild = p.deprecatedNode();
|
|
int offset = p.deprecatedEditingOffset();
|
|
|
|
if (canHaveChildrenForEditing(refChild)) {
|
|
Node* child = refChild->firstChild();
|
|
for (int i = 0; child && i < offset; i++)
|
|
child = child->nextSibling();
|
|
if (child)
|
|
insertNodeBefore(insertChild, child);
|
|
else
|
|
appendNode(insertChild, toContainerNode(refChild));
|
|
} else if (caretMinOffset(refChild) >= offset)
|
|
insertNodeBefore(insertChild, refChild);
|
|
else if (refChild->isTextNode() && caretMaxOffset(refChild) > offset) {
|
|
splitTextNode(toText(refChild), offset);
|
|
|
|
// Mutation events (bug 22634) from the text node insertion may have removed the refChild
|
|
if (!refChild->inDocument())
|
|
return;
|
|
insertNodeBefore(insertChild, refChild);
|
|
} else
|
|
insertNodeAfter(insertChild, refChild);
|
|
}
|
|
|
|
void CompositeEditCommand::appendNode(PassRefPtr<Node> node, PassRefPtr<ContainerNode> parent)
|
|
{
|
|
ASSERT(canHaveChildrenForEditing(parent.get()));
|
|
applyCommandToComposite(AppendNodeCommand::create(parent, node));
|
|
}
|
|
|
|
void CompositeEditCommand::removeChildrenInRange(PassRefPtr<Node> node, unsigned from, unsigned to)
|
|
{
|
|
Vector<RefPtr<Node> > children;
|
|
Node* child = NodeTraversal::childAt(*node, from);
|
|
for (unsigned i = from; child && i < to; i++, child = child->nextSibling())
|
|
children.append(child);
|
|
|
|
size_t size = children.size();
|
|
for (size_t i = 0; i < size; ++i)
|
|
removeNode(children[i].release());
|
|
}
|
|
|
|
void CompositeEditCommand::removeNode(PassRefPtr<Node> node, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable)
|
|
{
|
|
if (!node || !node->nonShadowBoundaryParentNode())
|
|
return;
|
|
applyCommandToComposite(RemoveNodeCommand::create(node, shouldAssumeContentIsAlwaysEditable));
|
|
}
|
|
|
|
void CompositeEditCommand::removeNodePreservingChildren(PassRefPtr<Node> node, ShouldAssumeContentIsAlwaysEditable shouldAssumeContentIsAlwaysEditable)
|
|
{
|
|
applyCommandToComposite(RemoveNodePreservingChildrenCommand::create(node, shouldAssumeContentIsAlwaysEditable));
|
|
}
|
|
|
|
void CompositeEditCommand::removeNodeAndPruneAncestors(PassRefPtr<Node> node, Node* excludeNode)
|
|
{
|
|
ASSERT(node.get() != excludeNode);
|
|
RefPtr<ContainerNode> parent = node->parentNode();
|
|
removeNode(node);
|
|
prune(parent.release(), excludeNode);
|
|
}
|
|
|
|
void CompositeEditCommand::moveRemainingSiblingsToNewParent(Node* node, Node* pastLastNodeToMove, PassRefPtr<Element> prpNewParent)
|
|
{
|
|
NodeVector nodesToRemove;
|
|
RefPtr<Element> newParent = prpNewParent;
|
|
|
|
for (; node && node != pastLastNodeToMove; node = node->nextSibling())
|
|
nodesToRemove.append(node);
|
|
|
|
for (unsigned i = 0; i < nodesToRemove.size(); i++) {
|
|
removeNode(nodesToRemove[i]);
|
|
appendNode(nodesToRemove[i], newParent);
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommand::updatePositionForNodeRemovalPreservingChildren(Position& position, Node& node)
|
|
{
|
|
int offset = (position.anchorType() == Position::PositionIsOffsetInAnchor) ? position.offsetInContainerNode() : 0;
|
|
updatePositionForNodeRemoval(position, node);
|
|
if (offset)
|
|
position.moveToOffset(offset);
|
|
}
|
|
|
|
void CompositeEditCommand::prune(PassRefPtr<Node> node, Node* excludeNode)
|
|
{
|
|
if (RefPtr<Node> highestNodeToRemove = highestNodeToRemoveInPruning(node.get(), excludeNode))
|
|
removeNode(highestNodeToRemove.release());
|
|
}
|
|
|
|
void CompositeEditCommand::splitTextNode(PassRefPtr<Text> node, unsigned offset)
|
|
{
|
|
applyCommandToComposite(SplitTextNodeCommand::create(node, offset));
|
|
}
|
|
|
|
void CompositeEditCommand::splitElement(PassRefPtr<Element> element, PassRefPtr<Node> atChild)
|
|
{
|
|
applyCommandToComposite(SplitElementCommand::create(element, atChild));
|
|
}
|
|
|
|
void CompositeEditCommand::insertTextIntoNode(PassRefPtr<Text> node, unsigned offset, const String& text)
|
|
{
|
|
if (!text.isEmpty())
|
|
applyCommandToComposite(InsertIntoTextNodeCommand::create(node, offset, text));
|
|
}
|
|
|
|
void CompositeEditCommand::deleteTextFromNode(PassRefPtr<Text> node, unsigned offset, unsigned count)
|
|
{
|
|
applyCommandToComposite(DeleteFromTextNodeCommand::create(node, offset, count));
|
|
}
|
|
|
|
void CompositeEditCommand::replaceTextInNode(PassRefPtr<Text> prpNode, unsigned offset, unsigned count, const String& replacementText)
|
|
{
|
|
RefPtr<Text> node(prpNode);
|
|
applyCommandToComposite(DeleteFromTextNodeCommand::create(node, offset, count));
|
|
if (!replacementText.isEmpty())
|
|
applyCommandToComposite(InsertIntoTextNodeCommand::create(node, offset, replacementText));
|
|
}
|
|
|
|
Position CompositeEditCommand::replaceSelectedTextInNode(const String& text)
|
|
{
|
|
Position start = endingSelection().start();
|
|
Position end = endingSelection().end();
|
|
if (start.containerNode() != end.containerNode() || !start.containerNode()->isTextNode())
|
|
return Position();
|
|
|
|
RefPtr<Text> textNode = start.containerText();
|
|
replaceTextInNode(textNode, start.offsetInContainerNode(), end.offsetInContainerNode() - start.offsetInContainerNode(), text);
|
|
|
|
return Position(textNode.release(), start.offsetInContainerNode() + text.length());
|
|
}
|
|
|
|
static void copyMarkerTypesAndDescriptions(const DocumentMarkerVector& markerPointers, Vector<DocumentMarker::MarkerType>& types, Vector<String>& descriptions)
|
|
{
|
|
size_t arraySize = markerPointers.size();
|
|
types.reserveCapacity(arraySize);
|
|
descriptions.reserveCapacity(arraySize);
|
|
for (size_t i = 0; i < arraySize; ++i) {
|
|
types.append(markerPointers[i]->type());
|
|
descriptions.append(markerPointers[i]->description());
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommand::replaceTextInNodePreservingMarkers(PassRefPtr<Text> prpNode, unsigned offset, unsigned count, const String& replacementText)
|
|
{
|
|
RefPtr<Text> node(prpNode);
|
|
DocumentMarkerController& markerController = document().markers();
|
|
Vector<DocumentMarker::MarkerType> types;
|
|
Vector<String> descriptions;
|
|
copyMarkerTypesAndDescriptions(markerController.markersInRange(Range::create(document(), node.get(), offset, node.get(), offset + count).get(), DocumentMarker::AllMarkers()), types, descriptions);
|
|
replaceTextInNode(node, offset, count, replacementText);
|
|
RefPtr<Range> newRange = Range::create(document(), node.get(), offset, node.get(), offset + replacementText.length());
|
|
ASSERT(types.size() == descriptions.size());
|
|
for (size_t i = 0; i < types.size(); ++i)
|
|
markerController.addMarker(newRange.get(), types[i], descriptions[i]);
|
|
}
|
|
|
|
Position CompositeEditCommand::positionOutsideTabSpan(const Position& pos)
|
|
{
|
|
return pos;
|
|
}
|
|
|
|
void CompositeEditCommand::insertNodeAtTabSpanPosition(PassRefPtr<Node> node, const Position& pos)
|
|
{
|
|
// insert node before, after, or at split of tab span
|
|
insertNodeAt(node, positionOutsideTabSpan(pos));
|
|
}
|
|
|
|
void CompositeEditCommand::deleteSelection(bool smartDelete, bool mergeBlocksAfterDelete, bool expandForSpecialElements, bool sanitizeMarkup)
|
|
{
|
|
if (endingSelection().isRange())
|
|
applyCommandToComposite(DeleteSelectionCommand::create(document(), smartDelete, mergeBlocksAfterDelete, expandForSpecialElements, sanitizeMarkup));
|
|
}
|
|
|
|
void CompositeEditCommand::deleteSelection(const VisibleSelection &selection, bool smartDelete, bool mergeBlocksAfterDelete, bool expandForSpecialElements, bool sanitizeMarkup)
|
|
{
|
|
if (selection.isRange())
|
|
applyCommandToComposite(DeleteSelectionCommand::create(selection, smartDelete, mergeBlocksAfterDelete, expandForSpecialElements, sanitizeMarkup));
|
|
}
|
|
|
|
static inline bool containsOnlyWhitespace(const String& text)
|
|
{
|
|
for (unsigned i = 0; i < text.length(); ++i) {
|
|
if (!isWhitespace(text[i]))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CompositeEditCommand::shouldRebalanceLeadingWhitespaceFor(const String& text) const
|
|
{
|
|
return containsOnlyWhitespace(text);
|
|
}
|
|
|
|
bool CompositeEditCommand::canRebalance(const Position& position) const
|
|
{
|
|
Node* node = position.containerNode();
|
|
if (position.anchorType() != Position::PositionIsOffsetInAnchor || !node || !node->isTextNode())
|
|
return false;
|
|
|
|
Text* textNode = toText(node);
|
|
if (textNode->length() == 0)
|
|
return false;
|
|
|
|
RenderText* renderer = textNode->renderer();
|
|
if (renderer && !renderer->style()->collapseWhiteSpace())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// FIXME: Doesn't go into text nodes that contribute adjacent text (siblings, cousins, etc).
|
|
void CompositeEditCommand::rebalanceWhitespaceAt(const Position& position)
|
|
{
|
|
Node* node = position.containerNode();
|
|
if (!canRebalance(position))
|
|
return;
|
|
|
|
// If the rebalance is for the single offset, and neither text[offset] nor text[offset - 1] are some form of whitespace, do nothing.
|
|
int offset = position.deprecatedEditingOffset();
|
|
String text = toText(node)->data();
|
|
if (!isWhitespace(text[offset])) {
|
|
offset--;
|
|
if (offset < 0 || !isWhitespace(text[offset]))
|
|
return;
|
|
}
|
|
|
|
rebalanceWhitespaceOnTextSubstring(toText(node), position.offsetInContainerNode(), position.offsetInContainerNode());
|
|
}
|
|
|
|
void CompositeEditCommand::rebalanceWhitespaceOnTextSubstring(PassRefPtr<Text> prpTextNode, int startOffset, int endOffset)
|
|
{
|
|
RefPtr<Text> textNode = prpTextNode;
|
|
|
|
String text = textNode->data();
|
|
ASSERT(!text.isEmpty());
|
|
|
|
// Set upstream and downstream to define the extent of the whitespace surrounding text[offset].
|
|
int upstream = startOffset;
|
|
while (upstream > 0 && isWhitespace(text[upstream - 1]))
|
|
upstream--;
|
|
|
|
int downstream = endOffset;
|
|
while ((unsigned)downstream < text.length() && isWhitespace(text[downstream]))
|
|
downstream++;
|
|
|
|
int length = downstream - upstream;
|
|
if (!length)
|
|
return;
|
|
|
|
VisiblePosition visibleUpstreamPos(Position(textNode, upstream));
|
|
VisiblePosition visibleDownstreamPos(Position(textNode, downstream));
|
|
|
|
String string = text.substring(upstream, length);
|
|
String rebalancedString = stringWithRebalancedWhitespace(string,
|
|
// FIXME: Because of the problem mentioned at the top of this function, we must also use nbsps at the start/end of the string because
|
|
// this function doesn't get all surrounding whitespace, just the whitespace in the current text node.
|
|
isStartOfParagraph(visibleUpstreamPos) || upstream == 0,
|
|
isEndOfParagraph(visibleDownstreamPos) || (unsigned)downstream == text.length());
|
|
|
|
if (string != rebalancedString)
|
|
replaceTextInNodePreservingMarkers(textNode.release(), upstream, length, rebalancedString);
|
|
}
|
|
|
|
void CompositeEditCommand::prepareWhitespaceAtPositionForSplit(Position& position)
|
|
{
|
|
Node* node = position.deprecatedNode();
|
|
if (!node || !node->isTextNode())
|
|
return;
|
|
Text* textNode = toText(node);
|
|
|
|
if (textNode->length() == 0)
|
|
return;
|
|
RenderText* renderer = textNode->renderer();
|
|
if (renderer && !renderer->style()->collapseWhiteSpace())
|
|
return;
|
|
|
|
// Delete collapsed whitespace so that inserting nbsps doesn't uncollapse it.
|
|
Position upstreamPos = position.upstream();
|
|
deleteInsignificantText(upstreamPos, position.downstream());
|
|
position = upstreamPos.downstream();
|
|
|
|
VisiblePosition visiblePos(position);
|
|
VisiblePosition previousVisiblePos(visiblePos.previous());
|
|
replaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(previousVisiblePos);
|
|
replaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(visiblePos);
|
|
}
|
|
|
|
void CompositeEditCommand::replaceCollapsibleWhitespaceWithNonBreakingSpaceIfNeeded(const VisiblePosition& visiblePosition)
|
|
{
|
|
if (!isCollapsibleWhitespace(visiblePosition.characterAfter()))
|
|
return;
|
|
Position pos = visiblePosition.deepEquivalent().downstream();
|
|
if (!pos.containerNode() || !pos.containerNode()->isTextNode())
|
|
return;
|
|
replaceTextInNodePreservingMarkers(pos.containerText(), pos.offsetInContainerNode(), 1, nonBreakingSpaceString());
|
|
}
|
|
|
|
void CompositeEditCommand::rebalanceWhitespace()
|
|
{
|
|
VisibleSelection selection = endingSelection();
|
|
if (selection.isNone())
|
|
return;
|
|
|
|
rebalanceWhitespaceAt(selection.start());
|
|
if (selection.isRange())
|
|
rebalanceWhitespaceAt(selection.end());
|
|
}
|
|
|
|
void CompositeEditCommand::deleteInsignificantText(PassRefPtr<Text> textNode, unsigned start, unsigned end)
|
|
{
|
|
if (!textNode || start >= end)
|
|
return;
|
|
|
|
document().updateLayout();
|
|
|
|
RenderText* textRenderer = textNode->renderer();
|
|
if (!textRenderer)
|
|
return;
|
|
|
|
Vector<InlineTextBox*> sortedTextBoxes;
|
|
size_t sortedTextBoxesPosition = 0;
|
|
|
|
for (InlineTextBox* textBox = textRenderer->firstTextBox(); textBox; textBox = textBox->nextTextBox())
|
|
sortedTextBoxes.append(textBox);
|
|
|
|
// If there is mixed directionality text, the boxes can be out of order,
|
|
// (like Arabic with embedded LTR), so sort them first.
|
|
if (textRenderer->containsReversedText())
|
|
std::sort(sortedTextBoxes.begin(), sortedTextBoxes.end(), InlineTextBox::compareByStart);
|
|
InlineTextBox* box = sortedTextBoxes.isEmpty() ? 0 : sortedTextBoxes[sortedTextBoxesPosition];
|
|
|
|
if (!box) {
|
|
// whole text node is empty
|
|
removeNode(textNode);
|
|
return;
|
|
}
|
|
|
|
unsigned length = textNode->length();
|
|
if (start >= length || end > length)
|
|
return;
|
|
|
|
unsigned removed = 0;
|
|
InlineTextBox* prevBox = 0;
|
|
String str;
|
|
|
|
// This loop structure works to process all gaps preceding a box,
|
|
// and also will look at the gap after the last box.
|
|
while (prevBox || box) {
|
|
unsigned gapStart = prevBox ? prevBox->start() + prevBox->len() : 0;
|
|
if (end < gapStart)
|
|
// No more chance for any intersections
|
|
break;
|
|
|
|
unsigned gapEnd = box ? box->start() : length;
|
|
bool indicesIntersect = start <= gapEnd && end >= gapStart;
|
|
int gapLen = gapEnd - gapStart;
|
|
if (indicesIntersect && gapLen > 0) {
|
|
gapStart = std::max(gapStart, start);
|
|
if (str.isNull())
|
|
str = textNode->data().substring(start, end - start);
|
|
// remove text in the gap
|
|
str.remove(gapStart - start - removed, gapLen);
|
|
removed += gapLen;
|
|
}
|
|
|
|
prevBox = box;
|
|
if (box) {
|
|
if (++sortedTextBoxesPosition < sortedTextBoxes.size())
|
|
box = sortedTextBoxes[sortedTextBoxesPosition];
|
|
else
|
|
box = 0;
|
|
}
|
|
}
|
|
|
|
if (!str.isNull()) {
|
|
// Replace the text between start and end with our pruned version.
|
|
if (!str.isEmpty())
|
|
replaceTextInNode(textNode, start, end - start, str);
|
|
else {
|
|
// Assert that we are not going to delete all of the text in the node.
|
|
// If we were, that should have been done above with the call to
|
|
// removeNode and return.
|
|
ASSERT(start > 0 || end - start < textNode->length());
|
|
deleteTextFromNode(textNode, start, end - start);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommand::deleteInsignificantText(const Position& start, const Position& end)
|
|
{
|
|
if (start.isNull() || end.isNull())
|
|
return;
|
|
|
|
if (comparePositions(start, end) >= 0)
|
|
return;
|
|
|
|
Vector<RefPtr<Text> > nodes;
|
|
for (Node* node = start.deprecatedNode(); node; node = NodeTraversal::next(*node)) {
|
|
if (node->isTextNode())
|
|
nodes.append(toText(node));
|
|
if (node == end.deprecatedNode())
|
|
break;
|
|
}
|
|
|
|
for (size_t i = 0; i < nodes.size(); ++i) {
|
|
Text* textNode = nodes[i].get();
|
|
int startOffset = textNode == start.deprecatedNode() ? start.deprecatedEditingOffset() : 0;
|
|
int endOffset = textNode == end.deprecatedNode() ? end.deprecatedEditingOffset() : static_cast<int>(textNode->length());
|
|
deleteInsignificantText(textNode, startOffset, endOffset);
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommand::deleteInsignificantTextDownstream(const Position& pos)
|
|
{
|
|
Position end = VisiblePosition(pos, VP_DEFAULT_AFFINITY).next().deepEquivalent().downstream();
|
|
deleteInsignificantText(pos, end);
|
|
}
|
|
|
|
// Assumes that the position is at a placeholder and does the removal without much checking.
|
|
void CompositeEditCommand::removePlaceholderAt(const Position& p)
|
|
{
|
|
ASSERT(lineBreakExistsAtPosition(p));
|
|
deleteTextFromNode(toText(p.anchorNode()), p.offsetInContainerNode(), 1);
|
|
}
|
|
|
|
void CompositeEditCommand::pushAnchorElementDown(Element* anchorNode)
|
|
{
|
|
if (!anchorNode)
|
|
return;
|
|
|
|
ASSERT(anchorNode->isLink());
|
|
|
|
setEndingSelection(VisibleSelection::selectionFromContentsOfNode(anchorNode));
|
|
// Clones of anchorNode have been pushed down, now remove it.
|
|
if (anchorNode->inDocument())
|
|
removeNodePreservingChildren(anchorNode);
|
|
}
|
|
|
|
// There are bugs in deletion when it removes a fully selected table/list.
|
|
// It expands and removes the entire table/list, but will let content
|
|
// before and after the table/list collapse onto one line.
|
|
// Deleting a paragraph will leave a placeholder. Remove it (and prune
|
|
// empty or unrendered parents).
|
|
|
|
void CompositeEditCommand::cleanupAfterDeletion(VisiblePosition destination)
|
|
{
|
|
VisiblePosition caretAfterDelete = endingSelection().visibleStart();
|
|
Node* destinationNode = destination.deepEquivalent().anchorNode();
|
|
if (caretAfterDelete != destination && isStartOfParagraph(caretAfterDelete) && isEndOfParagraph(caretAfterDelete)) {
|
|
// Note: We want the rightmost candidate.
|
|
Position position = caretAfterDelete.deepEquivalent().downstream();
|
|
Node* node = position.deprecatedNode();
|
|
|
|
// Bail if we'd remove an ancestor of our destination.
|
|
if (destinationNode && destinationNode->isDescendantOf(node))
|
|
return;
|
|
|
|
if (isBlock(node)) {
|
|
// If caret position after deletion and destination position coincides,
|
|
// node should not be removed.
|
|
if (!position.rendersInDifferentPosition(destination.deepEquivalent())) {
|
|
prune(node, destinationNode);
|
|
return;
|
|
}
|
|
removeNodeAndPruneAncestors(node, destinationNode);
|
|
}
|
|
else if (lineBreakExistsAtPosition(position)) {
|
|
// There is a preserved '\n' at caretAfterDelete.
|
|
// We can safely assume this is a text node.
|
|
Text* textNode = toText(node);
|
|
if (textNode->length() == 1)
|
|
removeNodeAndPruneAncestors(node, destinationNode);
|
|
else
|
|
deleteTextFromNode(textNode, position.deprecatedEditingOffset(), 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommand::moveParagraph(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, const VisiblePosition& destination, bool preserveSelection, bool preserveStyle, Node* constrainingAncestor)
|
|
{
|
|
ASSERT(isStartOfParagraph(startOfParagraphToMove));
|
|
ASSERT(isEndOfParagraph(endOfParagraphToMove));
|
|
moveParagraphs(startOfParagraphToMove, endOfParagraphToMove, destination, preserveSelection, preserveStyle, constrainingAncestor);
|
|
}
|
|
|
|
void CompositeEditCommand::moveParagraphs(const VisiblePosition& startOfParagraphToMove, const VisiblePosition& endOfParagraphToMove, const VisiblePosition& destination, bool preserveSelection, bool preserveStyle, Node* constrainingAncestor)
|
|
{
|
|
// FIXME(sky): Remove.
|
|
// We've probably broken editiing badly by deleting this function...
|
|
}
|
|
|
|
// Operations use this function to avoid inserting content into an anchor when at the start or the end of
|
|
// that anchor, as in NSTextView.
|
|
// FIXME: This is only an approximation of NSTextViews insertion behavior, which varies depending on how
|
|
// the caret was made.
|
|
Position CompositeEditCommand::positionAvoidingSpecialElementBoundary(const Position& original)
|
|
{
|
|
if (original.isNull())
|
|
return original;
|
|
|
|
VisiblePosition visiblePos(original);
|
|
Element* enclosingAnchor = enclosingAnchorElement(original);
|
|
Position result = original;
|
|
|
|
if (!enclosingAnchor)
|
|
return result;
|
|
|
|
// Don't avoid block level anchors, because that would insert content into the wrong paragraph.
|
|
if (enclosingAnchor && !isBlock(enclosingAnchor)) {
|
|
VisiblePosition firstInAnchor(firstPositionInNode(enclosingAnchor));
|
|
VisiblePosition lastInAnchor(lastPositionInNode(enclosingAnchor));
|
|
// If visually just after the anchor, insert *inside* the anchor unless it's the last
|
|
// VisiblePosition in the document, to match NSTextView.
|
|
if (visiblePos == lastInAnchor) {
|
|
// Make sure anchors are pushed down before avoiding them so that we don't
|
|
// also avoid structural elements like lists and blocks (5142012).
|
|
if (original.deprecatedNode() != enclosingAnchor && original.deprecatedNode()->parentNode() != enclosingAnchor) {
|
|
pushAnchorElementDown(enclosingAnchor);
|
|
enclosingAnchor = enclosingAnchorElement(original);
|
|
if (!enclosingAnchor)
|
|
return original;
|
|
}
|
|
// Don't insert outside an anchor if doing so would skip over a line break. It would
|
|
// probably be safe to move the line break so that we could still avoid the anchor here.
|
|
Position downstream(visiblePos.deepEquivalent().downstream());
|
|
if (lineBreakExistsAtVisiblePosition(visiblePos) && downstream.deprecatedNode()->isDescendantOf(enclosingAnchor))
|
|
return original;
|
|
|
|
result = positionInParentAfterNode(*enclosingAnchor);
|
|
}
|
|
// If visually just before an anchor, insert *outside* the anchor unless it's the first
|
|
// VisiblePosition in a paragraph, to match NSTextView.
|
|
if (visiblePos == firstInAnchor) {
|
|
// Make sure anchors are pushed down before avoiding them so that we don't
|
|
// also avoid structural elements like lists and blocks (5142012).
|
|
if (original.deprecatedNode() != enclosingAnchor && original.deprecatedNode()->parentNode() != enclosingAnchor) {
|
|
pushAnchorElementDown(enclosingAnchor);
|
|
enclosingAnchor = enclosingAnchorElement(original);
|
|
}
|
|
if (!enclosingAnchor)
|
|
return original;
|
|
|
|
result = positionInParentBeforeNode(*enclosingAnchor);
|
|
}
|
|
}
|
|
|
|
if (result.isNull() || !editableRootForPosition(result))
|
|
result = original;
|
|
|
|
return result;
|
|
}
|
|
|
|
// Splits the tree parent by parent until we reach the specified ancestor. We use VisiblePositions
|
|
// to determine if the split is necessary. Returns the last split node.
|
|
PassRefPtr<Node> CompositeEditCommand::splitTreeToNode(Node* start, Node* end, bool shouldSplitAncestor)
|
|
{
|
|
ASSERT(start);
|
|
ASSERT(end);
|
|
ASSERT(start != end);
|
|
|
|
if (shouldSplitAncestor && end->parentNode())
|
|
end = end->parentNode();
|
|
if (!start->isDescendantOf(end))
|
|
return end;
|
|
|
|
RefPtr<Node> endNode = end;
|
|
RefPtr<Node> node = nullptr;
|
|
for (node = start; node->parentNode() != endNode; node = node->parentNode()) {
|
|
Element* parentElement = node->parentElement();
|
|
if (!parentElement)
|
|
break;
|
|
// Do not split a node when doing so introduces an empty node.
|
|
VisiblePosition positionInParent(firstPositionInNode(parentElement));
|
|
VisiblePosition positionInNode(firstPositionInOrBeforeNode(node.get()));
|
|
if (positionInParent != positionInNode)
|
|
splitElement(parentElement, node);
|
|
}
|
|
|
|
return node.release();
|
|
}
|
|
|
|
} // namespace blink
|