Bug 1574852 - part 60: Move HTMLEditRules::MoveNodeSmart() and HTMLEditRules::MoveContents() to HTMLEditor r=m_kato

Making them return next insertion point with `nsresult` makes the callers
easier to understand.  Therefore, this patch declares `MoveNodeResult` class
newly and make them use it.

And also we can get rid of the case of setting `*aInOutDestOffset` to -1
because it's a hacky case of `HTMLEditRules::MoveBlock()`.  If we keep it,
`MoveNodeSmart()` needs to compute new insertion point with different logic.
Therefore, this patch makes `HTMLEditRules::MoveBlock()` adjust new insertion
point with checking its argument.

Differential Revision: https://phabricator.services.mozilla.com/D44199

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Masayuki Nakano 2019-09-04 03:32:26 +00:00
parent b9e615d3bd
commit e06533766b
5 changed files with 307 additions and 112 deletions

View File

@ -27,6 +27,38 @@ namespace mozilla {
using namespace dom;
/******************************************************************************
* mozilla::EditActionResult
*****************************************************************************/
EditActionResult& EditActionResult::operator|=(
const MoveNodeResult& aMoveNodeResult) {
mHandled |= aMoveNodeResult.Handled();
// When both result are same, keep the result.
if (mRv == aMoveNodeResult.Rv()) {
return *this;
}
// If one of the result is NS_ERROR_EDITOR_DESTROYED, use it since it's
// the most important error code for editor.
if (EditorDestroyed() || aMoveNodeResult.EditorDestroyed()) {
mRv = NS_ERROR_EDITOR_DESTROYED;
return *this;
}
// If aMoveNodeResult hasn't been set explicit nsresult value, keep current
// result.
if (aMoveNodeResult.Rv() == NS_ERROR_NOT_INITIALIZED) {
return *this;
}
// If one of the results is error, use NS_ERROR_FAILURE.
if (Failed() || aMoveNodeResult.Failed()) {
mRv = NS_ERROR_FAILURE;
return *this;
}
// Otherwise, use generic success code, NS_OK.
mRv = NS_OK;
return *this;
}
/******************************************************************************
* some helper classes for iterating the dom tree
*****************************************************************************/

View File

@ -24,6 +24,7 @@ class nsITransferable;
class nsRange;
namespace mozilla {
class MoveNodeResult;
template <class T>
class OwningNonNull;
@ -89,6 +90,8 @@ class MOZ_STACK_CLASS EditActionResult final {
return *this;
}
EditActionResult& operator|=(const MoveNodeResult& aMoveNodeResult);
private:
nsresult mRv;
bool mCanceled;
@ -175,6 +178,144 @@ class MOZ_STACK_CLASS CreateNodeResultBase final {
nsresult mRv;
};
/***************************************************************************
* MoveNodeResult is a simple class for MoveSomething() methods.
* This holds error code and next insertion point if moving contents succeeded.
*/
class MOZ_STACK_CLASS MoveNodeResult final {
public:
bool Succeeded() const { return NS_SUCCEEDED(mRv); }
bool Failed() const { return NS_FAILED(mRv); }
bool Handled() const { return mHandled; }
bool Ignored() const { return !mHandled; }
nsresult Rv() const { return mRv; }
bool EditorDestroyed() const { return mRv == NS_ERROR_EDITOR_DESTROYED; }
const EditorDOMPoint& NextInsertionPointRef() const {
return mNextInsertionPoint;
}
EditorDOMPoint NextInsertionPoint() const { return mNextInsertionPoint; }
MoveNodeResult() : mRv(NS_ERROR_NOT_INITIALIZED), mHandled(false) {}
explicit MoveNodeResult(nsresult aRv) : mRv(aRv), mHandled(false) {
MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(mRv));
}
MoveNodeResult(const MoveNodeResult& aOther) = delete;
MoveNodeResult& operator=(const MoveNodeResult& aOther) = delete;
MoveNodeResult(MoveNodeResult&& aOther) = default;
MoveNodeResult& operator=(MoveNodeResult&& aOther) = default;
MoveNodeResult& operator|=(const MoveNodeResult& aOther) {
mHandled |= aOther.mHandled;
// When both result are same, keep the result but use newer point.
if (mRv == aOther.mRv) {
mNextInsertionPoint = aOther.mNextInsertionPoint;
return *this;
}
// If one of the result is NS_ERROR_EDITOR_DESTROYED, use it since it's
// the most important error code for editor.
if (EditorDestroyed() || aOther.EditorDestroyed()) {
mRv = NS_ERROR_EDITOR_DESTROYED;
mNextInsertionPoint.Clear();
return *this;
}
// If the other one has not been set explicit nsresult, keep current
// value.
if (aOther.mRv == NS_ERROR_NOT_INITIALIZED) {
return *this;
}
// If this one has not been set explicit nsresult, copy the other one's.
if (mRv == NS_ERROR_NOT_INITIALIZED) {
mRv = aOther.mRv;
mNextInsertionPoint = aOther.mNextInsertionPoint;
return *this;
}
// If one of the results is error, use NS_ERROR_FAILURE.
if (Failed() || aOther.Failed()) {
mRv = NS_ERROR_FAILURE;
mNextInsertionPoint.Clear();
return *this;
}
// Otherwise, use generic success code, NS_OK, and use newer point.
mRv = NS_OK;
mNextInsertionPoint = aOther.mNextInsertionPoint;
return *this;
}
private:
template <typename PT, typename CT>
explicit MoveNodeResult(const EditorDOMPointBase<PT, CT>& aNextInsertionPoint,
bool aHandled)
: mNextInsertionPoint(aNextInsertionPoint),
mRv(aNextInsertionPoint.IsSet() ? NS_OK : NS_ERROR_FAILURE),
mHandled(aHandled && aNextInsertionPoint.IsSet()) {
if (mNextInsertionPoint.IsSet()) {
AutoEditorDOMPointChildInvalidator computeOffsetAndForgetChild(
mNextInsertionPoint);
}
}
MoveNodeResult(nsINode* aParentNode, uint32_t aOffsetOfNextInsertionPoint,
bool aHandled) {
if (!aParentNode) {
mRv = NS_ERROR_FAILURE;
mHandled = false;
return;
}
aOffsetOfNextInsertionPoint =
std::min(aOffsetOfNextInsertionPoint, aParentNode->Length());
mNextInsertionPoint.Set(aParentNode, aOffsetOfNextInsertionPoint);
mRv = mNextInsertionPoint.IsSet() ? NS_OK : NS_ERROR_FAILURE;
mHandled = aHandled && mNextInsertionPoint.IsSet();
}
EditorDOMPoint mNextInsertionPoint;
nsresult mRv;
bool mHandled;
friend MoveNodeResult MoveNodeIgnored(nsINode* aParentNode,
uint32_t aOffsetOfNextInsertionPoint);
friend MoveNodeResult MoveNodeHandled(nsINode* aParentNode,
uint32_t aOffsetOfNextInsertionPoint);
template <typename PT, typename CT>
friend MoveNodeResult MoveNodeIgnored(
const EditorDOMPointBase<PT, CT>& aNextInsertionPoint);
template <typename PT, typename CT>
friend MoveNodeResult MoveNodeHandled(
const EditorDOMPointBase<PT, CT>& aNextInsertionPoint);
};
/***************************************************************************
* When a move node handler (or its helper) does nothing,
* MoveNodeIgnored should be returned.
*/
inline MoveNodeResult MoveNodeIgnored(nsINode* aParentNode,
uint32_t aOffsetOfNextInsertionPoint) {
return MoveNodeResult(aParentNode, aOffsetOfNextInsertionPoint, false);
}
template <typename PT, typename CT>
inline MoveNodeResult MoveNodeIgnored(
const EditorDOMPointBase<PT, CT>& aNextInsertionPoint) {
return MoveNodeResult(aNextInsertionPoint, false);
}
/***************************************************************************
* When a move node handler (or its helper) handled and not canceled,
* MoveNodeHandled should be returned.
*/
inline MoveNodeResult MoveNodeHandled(nsINode* aParentNode,
uint32_t aOffsetOfNextInsertionPoint) {
return MoveNodeResult(aParentNode, aOffsetOfNextInsertionPoint, true);
}
template <typename PT, typename CT>
inline MoveNodeResult MoveNodeHandled(
const EditorDOMPointBase<PT, CT>& aNextInsertionPoint) {
return MoveNodeResult(aNextInsertionPoint, true);
}
/***************************************************************************
* SplitNodeResult is a simple class for
* EditorBase::SplitNodeDeepWithTransaction().

View File

@ -3593,18 +3593,23 @@ EditActionResult HTMLEditRules::TryToJoinBlocksWithTransaction(
HTMLEditorRef().GetInvisibleBRElementAt(leftBlockChild);
EditActionResult ret(NS_OK);
if (mergeLists) {
// XXX Why do we ignore the result of MoveContents()?
int32_t offset = leftBlockChild.Offset();
EditActionResult retMoveContents =
MoveContents(*rightList, *leftList, &offset);
if (NS_WARN_IF(retMoveContents.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return ret;
// XXX Why do we ignore the error from MoveChildren()?
// XXX Why is it guaranteed that `leftBlockChild.GetContainer()` is
// `leftList` here? Looks like that above code may run mutation
// event listeners.
NS_WARNING_ASSERTION(leftList == leftBlockChild.GetContainer(),
"This is not guaranteed, but assumed");
MoveNodeResult moveNodeResult =
MOZ_KnownLive(HTMLEditorRef())
.MoveChildren(*rightList,
EditorDOMPoint(leftList, leftBlockChild.Offset()));
if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(
retMoveContents.Succeeded(),
"Failed to move contents from the right list to the left list");
if (retMoveContents.Handled()) {
ret.MarkAsHandled();
NS_WARNING_ASSERTION(moveNodeResult.Succeeded(),
"MoveChildren() failed, but ignored");
if (moveNodeResult.Succeeded()) {
ret |= moveNodeResult;
}
// leftBlockChild was moved to rightList. So, it's invalid now.
leftBlockChild.Clear();
@ -3771,32 +3776,56 @@ EditActionResult HTMLEditRules::MoveBlock(Element& aLeftBlock,
return EditActionIgnored(rv);
}
uint32_t offset = static_cast<uint32_t>(aLeftOffset);
EditActionResult ret(NS_OK);
for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
for (auto& node : arrayOfNodes) {
if (aLeftOffset == -1) {
// For backward compatibility, we should move contents to end of the
// container if this is called with -1 for aLeftOffset.
offset = aLeftBlock.Length();
}
// get the node to act on
if (HTMLEditor::NodeIsBlockStatic(arrayOfNodes[i])) {
if (HTMLEditor::NodeIsBlockStatic(node)) {
// For block nodes, move their contents only, then delete block.
ret |= MoveContents(MOZ_KnownLive(*arrayOfNodes[i]->AsElement()),
aLeftBlock, &aLeftOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
MoveNodeResult moveNodeResult =
MOZ_KnownLive(HTMLEditorRef())
.MoveChildren(MOZ_KnownLive(*node->AsElement()),
EditorDOMPoint(&aLeftBlock, offset));
if (NS_WARN_IF(moveNodeResult.Failed())) {
return ret.SetResult(moveNodeResult.Rv());
}
rv = MOZ_KnownLive(HTMLEditorRef())
.DeleteNodeWithTransaction(MOZ_KnownLive(*arrayOfNodes[i]));
offset = moveNodeResult.NextInsertionPointRef().Offset();
ret |= moveNodeResult;
DebugOnly<nsresult> rvIgnored =
MOZ_KnownLive(HTMLEditorRef()).DeleteNodeWithTransaction(*node);
if (NS_WARN_IF(!CanHandleEditAction())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove a block node");
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"DeleteNodeWithTransaction() failed, but ignored");
ret.MarkAsHandled();
if (HTMLEditorRef().HasMutationEventListeners()) {
// Mutation event listener may make `offset` value invalid with
// removing some previous children while we call
// `DeleteNodeWithTransaction()` so that we should adjust it here.
offset = std::min(offset, aLeftBlock.Length());
}
} else {
// Otherwise move the content as is, checking against the DTD.
ret |= MoveNodeSmart(MOZ_KnownLive(*arrayOfNodes[i]->AsContent()),
aLeftBlock, &aLeftOffset);
if (NS_WARN_IF(ret.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return ret;
MoveNodeResult moveNodeResult =
MOZ_KnownLive(HTMLEditorRef())
.MoveNodeOrChildren(MOZ_KnownLive(*node->AsContent()),
EditorDOMPoint(&aLeftBlock, offset));
if (NS_WARN_IF(moveNodeResult.EditorDestroyed())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(moveNodeResult.Succeeded(),
"MoveNodeOrChildren() failed, but ignored");
if (moveNodeResult.Succeeded()) {
offset = moveNodeResult.NextInsertionPointRef().Offset();
ret |= moveNodeResult;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to move current node to the left block");
}
}
@ -3808,80 +3837,75 @@ EditActionResult HTMLEditRules::MoveBlock(Element& aLeftBlock,
return ret;
}
EditActionResult HTMLEditRules::MoveNodeSmart(nsIContent& aNode,
Element& aDestElement,
int32_t* aInOutDestOffset) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aInOutDestOffset);
MoveNodeResult HTMLEditor::MoveNodeOrChildren(
nsIContent& aContent, const EditorDOMPoint& aPointToInsert) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(aPointToInsert.IsSet());
// Check if this node can go into the destination node
if (HTMLEditorRef().CanContain(aDestElement, aNode)) {
if (CanContain(*aPointToInsert.GetContainer(), aContent)) {
// If it can, move it there.
if (*aInOutDestOffset == -1) {
nsresult rv = MOZ_KnownLive(HTMLEditorRef())
.MoveNodeToEndWithTransaction(aNode, aDestElement);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
} else {
EditorDOMPoint pointToInsert(&aDestElement, *aInOutDestOffset);
nsresult rv = MOZ_KnownLive(HTMLEditorRef())
.MoveNodeWithTransaction(aNode, pointToInsert);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
uint32_t offsetAtInserting = aPointToInsert.Offset();
nsresult rv = MoveNodeWithTransaction(aContent, aPointToInsert);
if (NS_WARN_IF(Destroyed())) {
return MoveNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (*aInOutDestOffset != -1) {
(*aInOutDestOffset)++;
if (NS_WARN_IF(NS_FAILED(rv))) {
return MoveNodeResult(rv);
}
// XXX Should we check if the node is actually moved in this case?
return EditActionHandled();
// Advance DOM point with offset for keeping backward compatibility.
// XXX Where should we insert next content if a mutation event listener
// break the relation of offset and moved node?
return MoveNodeHandled(aPointToInsert.GetContainer(), ++offsetAtInserting);
}
// If it can't, move its children (if any), and then delete it.
EditActionResult ret(NS_OK);
if (aNode.IsElement()) {
ret = MoveContents(MOZ_KnownLive(*aNode.AsElement()), aDestElement,
aInOutDestOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
MoveNodeResult result;
if (aContent.IsElement()) {
result = MoveChildren(MOZ_KnownLive(*aContent.AsElement()), aPointToInsert);
if (NS_WARN_IF(result.Failed())) {
return result;
}
} else {
result = MoveNodeHandled(aPointToInsert);
}
nsresult rv = MOZ_KnownLive(HTMLEditorRef()).DeleteNodeWithTransaction(aNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
nsresult rv = DeleteNodeWithTransaction(aContent);
if (NS_WARN_IF(Destroyed())) {
return MoveNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return ret.SetResult(rv);
return MoveNodeResult(rv);
}
return ret.MarkAsHandled();
}
EditActionResult HTMLEditRules::MoveContents(Element& aElement,
Element& aDestElement,
int32_t* aInOutDestOffset) {
MOZ_ASSERT(aInOutDestOffset);
if (NS_WARN_IF(&aElement == &aDestElement)) {
return EditActionIgnored(NS_ERROR_ILLEGAL_VALUE);
}
EditActionResult ret(NS_OK);
while (aElement.GetFirstChild()) {
ret |= MoveNodeSmart(MOZ_KnownLive(*aElement.GetFirstChild()), aDestElement,
aInOutDestOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
if (HasMutationEventListeners()) {
// Mutation event listener may make `offset` value invalid with
// removing some previous children while we call
// `DeleteNodeWithTransaction()` so that we should adjust it here.
if (!result.NextInsertionPointRef().IsSetAndValid()) {
result = MoveNodeHandled(aPointToInsert.GetContainer(),
aPointToInsert.GetContainer()->Length());
}
}
return ret;
return result;
}
MoveNodeResult HTMLEditor::MoveChildren(Element& aElement,
const EditorDOMPoint& aPointToInsert) {
MOZ_ASSERT(aPointToInsert.IsSet());
if (NS_WARN_IF(&aElement == aPointToInsert.GetContainer())) {
return MoveNodeResult(NS_ERROR_INVALID_ARG);
}
MoveNodeResult result = MoveNodeIgnored(aPointToInsert);
while (aElement.GetFirstChild()) {
result |= MoveNodeOrChildren(MOZ_KnownLive(*aElement.GetFirstChild()),
result.NextInsertionPoint());
if (NS_WARN_IF(result.Failed())) {
return result;
}
}
return result;
}
nsresult HTMLEditRules::DeleteElementsExceptTableRelatedElements(

View File

@ -189,34 +189,6 @@ class HTMLEditRules : public TextEditRules {
int32_t aLeftOffset,
int32_t aRightOffset);
/**
* MoveNodeSmart() moves aNode to (aDestElement, aInOutDestOffset).
* DTD containment rules are followed throughout.
*
* @param aOffset returns the point after inserted content.
* @return Sets true to handled if this actually moves
* the nodes.
* canceled is always false.
*/
MOZ_CAN_RUN_SCRIPT
MOZ_MUST_USE EditActionResult MoveNodeSmart(nsIContent& aNode,
Element& aDestElement,
int32_t* aInOutDestOffset);
/**
* MoveContents() moves the contents of aElement to (aDestElement,
* aInOutDestOffset). DTD containment rules are followed throughout.
*
* @param aInOutDestOffset updated to point after inserted content.
* @return Sets true to handled if this actually moves
* the nodes.
* canceled is always false.
*/
MOZ_CAN_RUN_SCRIPT
MOZ_MUST_USE EditActionResult MoveContents(Element& aElement,
Element& aDestElement,
int32_t* aInOutDestOffset);
/**
* DeleteElementsExceptTableRelatedElements() removes elements except
* table related elements (except <table> itself) and their contents

View File

@ -44,6 +44,7 @@ class AutoSelectionSetterAfterTableEdit;
class AutoSetTemporaryAncestorLimiter;
class EditActionResult;
class EmptyEditableFunctor;
class MoveNodeResult;
class ResizerSelectionListener;
class SplitRangeOffFromNodeResult;
enum class EditSubAction : int32_t;
@ -1988,6 +1989,31 @@ class HTMLEditor final : public TextEditor,
nsIContent& aLeftNode, nsIContent& aRightNode,
EditorDOMPoint* aNewFirstChildOfRightNode);
/**
* MoveNodeOrChildren() moves aContent to aPointToInsert. If cannot insert
* aContent due to invalid relation, moves only its children recursively
* and removes aContent from the DOM tree.
*
* @param aContent Content which should be moved.
* @param aPointToInsert The point to be inserted aContent or its
* descendants.
*/
MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE MoveNodeResult
MoveNodeOrChildren(nsIContent& aNode, const EditorDOMPoint& aPointToInsert);
/**
* MoveContents() moves the children of aElement to aPointToInsert. If
* cannot insert some children due to invalid relation, calls
* MoveNodeOrChildren() to remove the children but keep moving its children.
*
* @param aElement Container element whose children should be
* moved.
* @param aPointToInsert The point to be inserted children of aElement
* or its descendants.
*/
MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE MoveNodeResult
MoveChildren(Element& aElement, const EditorDOMPoint& aPointToInsert);
protected: // Called by helper classes.
virtual void OnStartToHandleTopLevelEditSubAction(
EditSubAction aEditSubAction, nsIEditor::EDirection aDirection) override;