/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "HTMLEditRules.h"
#include <stdlib.h>
#include "HTMLEditUtils.h"
#include "TextEditUtils.h"
#include "WSRunObject.h"
#include "mozilla/Assertions.h"
#include "mozilla/ContentIterator.h"
#include "mozilla/CSSEditUtils.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorUtils.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Move.h"
#include "mozilla/Preferences.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/RangeBinding.h"
#include "mozilla/OwningNonNull.h"
#include "mozilla/mozalloc.h"
#include "nsAString.h"
#include "nsAlgorithm.h"
#include "nsCRT.h"
#include "nsCRTGlue.h"
#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsAtom.h"
#include "nsHTMLDocument.h"
#include "nsIContent.h"
#include "nsID.h"
#include "nsIFrame.h"
#include "nsIHTMLAbsPosEditor.h"
#include "nsINode.h"
#include "nsLiteralString.h"
#include "nsRange.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTArray.h"
#include "nsTextNode.h"
#include "nsThreadUtils.h"
#include "nsUnicharUtils.h"
#include <algorithm>
// Workaround for windows headers
#ifdef SetProp
# undef SetProp
#endif
class nsISupports;
namespace mozilla {
using namespace dom;
// const static char* kMOZEditorBogusNodeAttr="MOZ_EDITOR_BOGUS_NODE";
// const static char* kMOZEditorBogusNodeValue="TRUE";
enum { kLonely = 0, kPrevSib = 1, kNextSib = 2, kBothSibs = 3 };
/********************************************************
* first some helpful functors we will use
********************************************************/
static bool IsStyleCachePreservingSubAction(EditSubAction aEditSubAction) {
return aEditSubAction == EditSubAction::eDeleteSelectedContent ||
aEditSubAction == EditSubAction::eInsertLineBreak ||
aEditSubAction == EditSubAction::eInsertParagraphSeparator ||
aEditSubAction == EditSubAction::eCreateOrChangeList ||
aEditSubAction == EditSubAction::eIndent ||
aEditSubAction == EditSubAction::eOutdent ||
aEditSubAction == EditSubAction::eSetOrClearAlignment ||
aEditSubAction == EditSubAction::eCreateOrRemoveBlock ||
aEditSubAction == EditSubAction::eRemoveList ||
aEditSubAction == EditSubAction::eCreateOrChangeDefinitionList ||
aEditSubAction == EditSubAction::eInsertElement ||
aEditSubAction == EditSubAction::eInsertQuotation;
}
static nsAtom& ParagraphSeparatorElement(ParagraphSeparator separator) {
switch (separator) {
default:
MOZ_FALLTHROUGH_ASSERT("Unexpected paragraph separator!");
case ParagraphSeparator::div:
return *nsGkAtoms::div;
case ParagraphSeparator::p:
return *nsGkAtoms::p;
case ParagraphSeparator::br:
return *nsGkAtoms::br;
}
}
class TableCellAndListItemFunctor final : public BoolDomIterFunctor {
public:
// Used to build list of all li's, td's & th's iterator covers
virtual bool operator()(nsINode* aNode) const override {
return HTMLEditUtils::IsTableCell(aNode) ||
HTMLEditUtils::IsListItem(aNode);
}
};
class BRNodeFunctor final : public BoolDomIterFunctor {
public:
virtual bool operator()(nsINode* aNode) const override {
return aNode->IsHTMLElement(nsGkAtoms::br);
}
};
class EmptyEditableFunctor final : public BoolDomIterFunctor {
public:
explicit EmptyEditableFunctor(HTMLEditor* aHTMLEditor)
: mHTMLEditor(aHTMLEditor) {}
virtual bool operator()(nsINode* aNode) const override {
if (mHTMLEditor->IsEditable(aNode) &&
(HTMLEditUtils::IsListItem(aNode) ||
HTMLEditUtils::IsTableCellOrCaption(*aNode))) {
bool bIsEmptyNode;
nsresult rv =
mHTMLEditor->IsEmptyNode(aNode, &bIsEmptyNode, false, false);
NS_ENSURE_SUCCESS(rv, false);
if (bIsEmptyNode) {
return true;
}
}
return false;
}
protected:
HTMLEditor* mHTMLEditor;
};
class MOZ_RAII AutoSetTemporaryAncestorLimiter final {
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
public:
explicit AutoSetTemporaryAncestorLimiter(
HTMLEditor& aHTMLEditor, Selection& aSelection,
nsINode& aStartPointNode MOZ_GUARD_OBJECT_NOTIFIER_PARAM) {
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
if (aSelection.GetAncestorLimiter()) {
return;
}
Element* root = aHTMLEditor.FindSelectionRoot(&aStartPointNode);
if (root) {
aHTMLEditor.InitializeSelectionAncestorLimit(*root);
mSelection = &aSelection;
}
}
~AutoSetTemporaryAncestorLimiter() {
if (mSelection) {
mSelection->SetAncestorLimiter(nullptr);
}
}
private:
RefPtr<Selection> mSelection;
};
/********************************************************
* mozilla::HTMLEditRules
********************************************************/
HTMLEditRules::HTMLEditRules()
: mHTMLEditor(nullptr),
mListenerEnabled(false),
mReturnInEmptyLIKillsList(false),
mDidDeleteSelection(false),
mDidRangedDelete(false),
mRestoreContentEditableCount(false),
mJoinOffset(0) {
mIsHTMLEditRules = true;
InitFields();
}
void HTMLEditRules::InitFields() {
mHTMLEditor = nullptr;
mDocChangeRange = nullptr;
mReturnInEmptyLIKillsList = true;
mDidDeleteSelection = false;
mDidRangedDelete = false;
mRestoreContentEditableCount = false;
mUtilRange = nullptr;
mJoinOffset = 0;
mNewBlock = nullptr;
mRangeItem = new RangeItem();
InitStyleCacheArray(mCachedStyles);
}
void HTMLEditRules::InitStyleCacheArray(
StyleCache aStyleCache[SIZE_STYLE_TABLE]) {
aStyleCache[0] = StyleCache(nsGkAtoms::b, nullptr);
aStyleCache[1] = StyleCache(nsGkAtoms::i, nullptr);
aStyleCache[2] = StyleCache(nsGkAtoms::u, nullptr);
aStyleCache[3] = StyleCache(nsGkAtoms::font, nsGkAtoms::face);
aStyleCache[4] = StyleCache(nsGkAtoms::font, nsGkAtoms::size);
aStyleCache[5] = StyleCache(nsGkAtoms::font, nsGkAtoms::color);
aStyleCache[6] = StyleCache(nsGkAtoms::tt, nullptr);
aStyleCache[7] = StyleCache(nsGkAtoms::em, nullptr);
aStyleCache[8] = StyleCache(nsGkAtoms::strong, nullptr);
aStyleCache[9] = StyleCache(nsGkAtoms::dfn, nullptr);
aStyleCache[10] = StyleCache(nsGkAtoms::code, nullptr);
aStyleCache[11] = StyleCache(nsGkAtoms::samp, nullptr);
aStyleCache[12] = StyleCache(nsGkAtoms::var, nullptr);
aStyleCache[13] = StyleCache(nsGkAtoms::cite, nullptr);
aStyleCache[14] = StyleCache(nsGkAtoms::abbr, nullptr);
aStyleCache[15] = StyleCache(nsGkAtoms::acronym, nullptr);
aStyleCache[16] = StyleCache(nsGkAtoms::backgroundColor, nullptr);
aStyleCache[17] = StyleCache(nsGkAtoms::sub, nullptr);
aStyleCache[18] = StyleCache(nsGkAtoms::sup, nullptr);
}
HTMLEditRules::~HTMLEditRules() {}
NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLEditRules, TextEditRules)
NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules,
mDocChangeRange, mUtilRange, mNewBlock,
mRangeItem)
nsresult HTMLEditRules::Init(TextEditor* aTextEditor) {
if (NS_WARN_IF(!aTextEditor) || NS_WARN_IF(!aTextEditor->AsHTMLEditor())) {
return NS_ERROR_INVALID_ARG;
}
InitFields();
mHTMLEditor = aTextEditor->AsHTMLEditor();
if (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor);
nsresult rv = TextEditRules::Init(aTextEditor);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_FAILURE;
}
// cache any prefs we care about
static const char kPrefName[] =
"editor.html.typing.returnInEmptyListItemClosesList";
nsAutoCString returnInEmptyLIKillsList;
Preferences::GetCString(kPrefName, returnInEmptyLIKillsList);
// only when "false", becomes FALSE. Otherwise (including empty), TRUE.
// XXX Why was this pref designed as a string and not bool?
mReturnInEmptyLIKillsList = !returnInEmptyLIKillsList.EqualsLiteral("false");
// make a utility range for use by the listenter
nsCOMPtr<nsINode> node = HTMLEditorRef().GetRoot();
if (!node) {
node = HTMLEditorRef().GetDocument();
if (NS_WARN_IF(!node)) {
return NS_ERROR_FAILURE;
}
}
mUtilRange = new nsRange(node);
// set up mDocChangeRange to be whole doc
// temporarily turn off rules sniffing
AutoLockRulesSniffing lockIt(this);
if (!mDocChangeRange) {
mDocChangeRange = new nsRange(node);
}
if (node->IsElement()) {
ErrorResult error;
mDocChangeRange->SelectNode(*node, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInChangedRange();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to insert <br> elements to empty list items and table cells");
}
StartToListenToEditSubActions();
return NS_OK;
}
nsresult HTMLEditRules::DetachEditor() {
EndListeningToEditSubActions();
mHTMLEditor = nullptr;
return TextEditRules::DetachEditor();
}
nsresult HTMLEditRules::BeforeEdit(EditSubAction aEditSubAction,
nsIEditor::EDirection aDirection) {
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (mLockRulesSniffing) {
return NS_OK;
}
AutoLockRulesSniffing lockIt(this);
mDidExplicitlySetInterline = false;
if (!mActionNesting) {
mActionNesting++;
// Clear our flag about if just deleted a range
mDidRangedDelete = false;
AutoSafeEditorData setData(*this, *mHTMLEditor);
// Remember where our selection was before edit action took place:
// Get the selection location
if (!SelectionRefPtr()->RangeCount()) {
return NS_ERROR_UNEXPECTED;
}
mRangeItem->StoreRange(SelectionRefPtr()->GetRangeAt(0));
nsCOMPtr<nsINode> selStartNode = mRangeItem->mStartContainer;
nsCOMPtr<nsINode> selEndNode = mRangeItem->mEndContainer;
// Register with range updater to track this as we perturb the doc
HTMLEditorRef().RangeUpdaterRef().RegisterRangeItem(mRangeItem);
// Clear deletion state bool
mDidDeleteSelection = false;
// Clear out mDocChangeRange and mUtilRange
if (mDocChangeRange) {
// Clear out our accounting of what changed
mDocChangeRange->Reset();
}
if (mUtilRange) {
// Ditto for mUtilRange.
mUtilRange->Reset();
}
// Remember current inline styles for deletion and normal insertion ops
if (aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME ||
aEditSubAction == EditSubAction::eDeleteSelectedContent ||
IsStyleCachePreservingSubAction(aEditSubAction)) {
nsCOMPtr<nsINode> selNode =
aDirection == nsIEditor::eNext ? selEndNode : selStartNode;
nsresult rv = CacheInlineStyles(selNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Stabilize the document against contenteditable count changes
nsHTMLDocument* htmlDoc = HTMLEditorRef().GetHTMLDocument();
if (NS_WARN_IF(!htmlDoc)) {
return NS_ERROR_FAILURE;
}
if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
htmlDoc->ChangeContentEditableCount(nullptr, +1);
mRestoreContentEditableCount = true;
}
// Check that selection is in subtree defined by body node
nsresult rv = ConfirmSelectionInBody();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
// Let rules remember the top level action
mTopLevelEditSubAction = aEditSubAction;
}
return NS_OK;
}
nsresult HTMLEditRules::AfterEdit(EditSubAction aEditSubAction,
nsIEditor::EDirection aDirection) {
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (mLockRulesSniffing) {
return NS_OK;
}
AutoLockRulesSniffing lockIt(this);
MOZ_ASSERT(mActionNesting > 0);
nsresult rv = NS_OK;
mActionNesting--;
if (!mActionNesting) {
AutoSafeEditorData setData(*this, *mHTMLEditor);
// Do all the tricky stuff
rv = AfterEditInner(aEditSubAction, aDirection);
// Perhaps, we need to do the following jobs even if the editor has been
// destroyed since they adjust some states of HTML document but don't
// modify the DOM tree nor Selection.
// Free up selectionState range item
HTMLEditorRef().RangeUpdaterRef().DropRangeItem(mRangeItem);
// Reset the contenteditable count to its previous value
if (mRestoreContentEditableCount) {
nsHTMLDocument* htmlDoc = HTMLEditorRef().GetHTMLDocument();
if (NS_WARN_IF(!htmlDoc)) {
return NS_ERROR_FAILURE;
}
if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
htmlDoc->ChangeContentEditableCount(nullptr, -1);
}
mRestoreContentEditableCount = false;
}
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::AfterEditInner(EditSubAction aEditSubAction,
nsIEditor::EDirection aDirection) {
MOZ_ASSERT(IsEditorDataAvailable());
nsresult rv = ConfirmSelectionInBody();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to normalize Selection");
if (aEditSubAction == EditSubAction::eReplaceHeadWithHTMLSource ||
aEditSubAction == EditSubAction::eCreateBogusNode) {
return NS_OK;
}
nsCOMPtr<nsINode> rangeStartContainer, rangeEndContainer;
uint32_t rangeStartOffset = 0, rangeEndOffset = 0;
// do we have a real range to act on?
bool bDamagedRange = false;
if (mDocChangeRange) {
rangeStartContainer = mDocChangeRange->GetStartContainer();
rangeEndContainer = mDocChangeRange->GetEndContainer();
rangeStartOffset = mDocChangeRange->StartOffset();
rangeEndOffset = mDocChangeRange->EndOffset();
if (rangeStartContainer && rangeEndContainer) {
bDamagedRange = true;
}
}
if (bDamagedRange && !((aEditSubAction == EditSubAction::eUndo) ||
(aEditSubAction == EditSubAction::eRedo))) {
// don't let any txns in here move the selection around behind our back.
// Note that this won't prevent explicit selection setting from working.
AutoTransactionsConserveSelection dontChangeMySelection(HTMLEditorRef());
// expand the "changed doc range" as needed
PromoteRange(*mDocChangeRange, aEditSubAction);
// if we did a ranged deletion or handling backspace key, make sure we have
// a place to put caret.
// Note we only want to do this if the overall operation was deletion,
// not if deletion was done along the way for
// EditSubAction::eInsertHTMLSource, EditSubAction::eInsertText, etc.
// That's why this is here rather than DidDeleteSelection().
if (aEditSubAction == EditSubAction::eDeleteSelectedContent &&
mDidRangedDelete) {
nsresult rv = InsertBRIfNeeded();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// add in any needed <br>s, and remove any unneeded ones.
nsresult rv = InsertBRElementToEmptyListItemsAndTableCellsInChangedRange();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to insert <br> elements to empty list items and table cells");
// merge any adjacent text nodes
if (aEditSubAction != EditSubAction::eInsertText &&
aEditSubAction != EditSubAction::eInsertTextComingFromIME) {
nsresult rv = HTMLEditorRef().CollapseAdjacentTextNodes(mDocChangeRange);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// clean up any empty nodes in the selection
rv = RemoveEmptyNodesInChangedRange();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// attempt to transform any unneeded nbsp's into spaces after doing various
// operations
if (aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME ||
aEditSubAction == EditSubAction::eDeleteSelectedContent ||
aEditSubAction == EditSubAction::eInsertLineBreak ||
aEditSubAction == EditSubAction::eInsertParagraphSeparator ||
aEditSubAction == EditSubAction::ePasteHTMLContent ||
aEditSubAction == EditSubAction::eInsertHTMLSource) {
rv = AdjustWhitespace();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// also do this for original selection endpoints.
NS_ENSURE_STATE(mRangeItem->mStartContainer);
NS_ENSURE_STATE(mRangeItem->mEndContainer);
WSRunObject(&HTMLEditorRef(), mRangeItem->mStartContainer,
mRangeItem->mStartOffset)
.AdjustWhitespace();
// we only need to handle old selection endpoint if it was different from
// start
if (mRangeItem->mStartContainer != mRangeItem->mEndContainer ||
mRangeItem->mStartOffset != mRangeItem->mEndOffset) {
WSRunObject(&HTMLEditorRef(), mRangeItem->mEndContainer,
mRangeItem->mEndOffset)
.AdjustWhitespace();
}
}
// if we created a new block, make sure selection lands in it
if (mNewBlock) {
rv = PinSelectionToNewBlock();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to pin selection to the new block");
mNewBlock = nullptr;
}
// adjust selection for insert text, html paste, and delete actions
if (aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME ||
aEditSubAction == EditSubAction::eDeleteSelectedContent ||
aEditSubAction == EditSubAction::eInsertLineBreak ||
aEditSubAction == EditSubAction::eInsertParagraphSeparator ||
aEditSubAction == EditSubAction::ePasteHTMLContent ||
aEditSubAction == EditSubAction::eInsertHTMLSource) {
rv = AdjustSelection(aDirection);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// check for any styles which were removed inappropriately
if (aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME ||
aEditSubAction == EditSubAction::eDeleteSelectedContent ||
IsStyleCachePreservingSubAction(aEditSubAction)) {
HTMLEditorRef().mTypeInState->UpdateSelState(SelectionRefPtr());
rv = ReapplyCachedStyles();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
ClearCachedStyles();
}
}
rv = HTMLEditorRef().HandleInlineSpellCheck(
aEditSubAction, mRangeItem->mStartContainer, mRangeItem->mStartOffset,
rangeStartContainer, rangeStartOffset, rangeEndContainer, rangeEndOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// detect empty doc
rv = CreateBogusNodeIfNeeded();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// adjust selection HINT if needed
if (!mDidExplicitlySetInterline) {
CheckInterlinePosition();
}
return NS_OK;
}
nsresult HTMLEditRules::WillDoAction(EditSubActionInfo& aInfo, bool* aCancel,
bool* aHandled) {
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
MOZ_ASSERT(aCancel);
MOZ_ASSERT(aHandled);
*aCancel = false;
*aHandled = false;
// Deal with actions for which we don't need to check whether the selection is
// editable.
if (aInfo.mEditSubAction == EditSubAction::eComputeTextToOutput ||
aInfo.mEditSubAction == EditSubAction::eUndo ||
aInfo.mEditSubAction == EditSubAction::eRedo) {
return TextEditRules::WillDoAction(aInfo, aCancel, aHandled);
}
AutoSafeEditorData setData(*this, *mHTMLEditor);
// Nothing to do if there's no selection to act on
if (NS_WARN_IF(!SelectionRefPtr()->RangeCount())) {
return NS_OK;
}
RefPtr<nsRange> range = SelectionRefPtr()->GetRangeAt(0);
nsCOMPtr<nsINode> selStartNode = range->GetStartContainer();
if (NS_WARN_IF(!selStartNode)) {
return NS_ERROR_FAILURE;
}
if (!HTMLEditorRef().IsModifiableNode(*selStartNode)) {
*aCancel = true;
return NS_OK;
}
nsCOMPtr<nsINode> selEndNode = range->GetEndContainer();
if (NS_WARN_IF(!selEndNode)) {
return NS_ERROR_FAILURE;
}
if (selStartNode != selEndNode) {
if (!HTMLEditorRef().IsModifiableNode(*selEndNode)) {
*aCancel = true;
return NS_OK;
}
nsINode* commonAncestor = range->GetCommonAncestor();
if (NS_WARN_IF(!commonAncestor)) {
return NS_ERROR_FAILURE;
}
if (!HTMLEditorRef().IsModifiableNode(*commonAncestor)) {
*aCancel = true;
return NS_OK;
}
}
switch (aInfo.mEditSubAction) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertTextComingFromIME:
UndefineCaretBidiLevel();
return WillInsertText(aInfo.mEditSubAction, aCancel, aHandled,
aInfo.inString, aInfo.outString, aInfo.maxLength);
case EditSubAction::eInsertHTMLSource:
return WillLoadHTML();
case EditSubAction::eInsertParagraphSeparator: {
UndefineCaretBidiLevel();
EditActionResult result = WillInsertParagraphSeparator();
if (NS_WARN_IF(result.Failed())) {
return result.Rv();
}
*aCancel = result.Canceled();
*aHandled = result.Handled();
MOZ_ASSERT(!result.Ignored());
return NS_OK;
}
case EditSubAction::eDeleteSelectedContent:
return WillDeleteSelection(aInfo.collapsedAction, aInfo.stripWrappers,
aCancel, aHandled);
case EditSubAction::eCreateOrChangeList:
return WillMakeList(aInfo.blockType, aInfo.entireList, aInfo.bulletType,
aCancel, aHandled);
case EditSubAction::eIndent:
return WillIndent(aCancel, aHandled);
case EditSubAction::eOutdent:
return WillOutdent(aCancel, aHandled);
case EditSubAction::eSetPositionToAbsolute:
return WillAbsolutePosition(aCancel, aHandled);
case EditSubAction::eSetPositionToStatic:
return WillRemoveAbsolutePosition(aCancel, aHandled);
case EditSubAction::eSetOrClearAlignment:
return WillAlign(*aInfo.alignType, aCancel, aHandled);
case EditSubAction::eCreateOrRemoveBlock:
return WillMakeBasicBlock(*aInfo.blockType, aCancel, aHandled);
case EditSubAction::eRemoveList: {
nsresult rv = WillRemoveList(aCancel, aHandled);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) ||
NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
case EditSubAction::eCreateOrChangeDefinitionList:
return WillMakeDefListItem(aInfo.blockType, aInfo.entireList, aCancel,
aHandled);
case EditSubAction::eInsertElement: {
nsresult rv = WillInsert(aCancel);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
return NS_OK;
}
case EditSubAction::eDecreaseZIndex:
return WillRelativeChangeZIndex(-1, aCancel, aHandled);
case EditSubAction::eIncreaseZIndex:
return WillRelativeChangeZIndex(1, aCancel, aHandled);
default:
return TextEditRules::WillDoAction(aInfo, aCancel, aHandled);
}
}
nsresult HTMLEditRules::DidDoAction(EditSubActionInfo& aInfo,
nsresult aResult) {
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
AutoSafeEditorData setData(*this, *mHTMLEditor);
switch (aInfo.mEditSubAction) {
case EditSubAction::eInsertText:
case EditSubAction::eInsertLineBreak:
case EditSubAction::eInsertParagraphSeparator:
case EditSubAction::eInsertTextComingFromIME:
return NS_OK;
case EditSubAction::eDeleteSelectedContent:
return DidDeleteSelection();
case EditSubAction::eCreateOrRemoveBlock:
case EditSubAction::eIndent:
case EditSubAction::eOutdent:
case EditSubAction::eSetOrClearAlignment:
return DidMakeBasicBlock();
case EditSubAction::eSetPositionToAbsolute: {
nsresult rv = DidMakeBasicBlock();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return DidAbsolutePosition();
}
default:
return TextEditRules::DidDoAction(aInfo, aResult);
}
}
bool HTMLEditRules::DocumentIsEmpty() { return !!mBogusNode; }
nsresult HTMLEditRules::GetListState(bool* aMixed, bool* aOL, bool* aUL,
bool* aDL) {
NS_ENSURE_TRUE(aMixed && aOL && aUL && aDL, NS_ERROR_NULL_POINTER);
*aMixed = false;
*aOL = false;
*aUL = false;
*aDL = false;
bool bNonList = false;
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
AutoSafeEditorData setData(*this, *mHTMLEditor);
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv =
GetListActionNodes(arrayOfNodes, EntireList::no, TouchContent::no);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Examine list type for nodes in selection.
for (const auto& curNode : arrayOfNodes) {
if (!curNode->IsElement()) {
bNonList = true;
} else if (curNode->IsHTMLElement(nsGkAtoms::ul)) {
*aUL = true;
} else if (curNode->IsHTMLElement(nsGkAtoms::ol)) {
*aOL = true;
} else if (curNode->IsHTMLElement(nsGkAtoms::li)) {
if (dom::Element* parent = curNode->GetParentElement()) {
if (parent->IsHTMLElement(nsGkAtoms::ul)) {
*aUL = true;
} else if (parent->IsHTMLElement(nsGkAtoms::ol)) {
*aOL = true;
}
}
} else if (curNode->IsAnyOfHTMLElements(nsGkAtoms::dl, nsGkAtoms::dt,
nsGkAtoms::dd)) {
*aDL = true;
} else {
bNonList = true;
}
}
// hokey arithmetic with booleans
if ((*aUL + *aOL + *aDL + bNonList) > 1) {
*aMixed = true;
}
return NS_OK;
}
nsresult HTMLEditRules::GetListItemState(bool* aMixed, bool* aLI, bool* aDT,
bool* aDD) {
NS_ENSURE_TRUE(aMixed && aLI && aDT && aDD, NS_ERROR_NULL_POINTER);
*aMixed = false;
*aLI = false;
*aDT = false;
*aDD = false;
bool bNonList = false;
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
AutoSafeEditorData setData(*this, *mHTMLEditor);
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv =
GetListActionNodes(arrayOfNodes, EntireList::no, TouchContent::no);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// examine list type for nodes in selection
for (const auto& node : arrayOfNodes) {
if (!node->IsElement()) {
bNonList = true;
} else if (node->IsAnyOfHTMLElements(nsGkAtoms::ul, nsGkAtoms::ol,
nsGkAtoms::li)) {
*aLI = true;
} else if (node->IsHTMLElement(nsGkAtoms::dt)) {
*aDT = true;
} else if (node->IsHTMLElement(nsGkAtoms::dd)) {
*aDD = true;
} else if (node->IsHTMLElement(nsGkAtoms::dl)) {
// need to look inside dl and see which types of items it has
bool bDT, bDD;
GetDefinitionListItemTypes(node->AsElement(), &bDT, &bDD);
*aDT |= bDT;
*aDD |= bDD;
} else {
bNonList = true;
}
}
// hokey arithmetic with booleans
if (*aDT + *aDD + bNonList > 1) {
*aMixed = true;
}
return NS_OK;
}
nsresult HTMLEditRules::GetAlignment(bool* aMixed,
nsIHTMLEditor::EAlignment* aAlign) {
MOZ_ASSERT(aMixed && aAlign);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
AutoSafeEditorData setData(*this, *mHTMLEditor);
// For now, just return first alignment. We'll lie about if it's mixed.
// This is for efficiency given that our current ui doesn't care if it's
// mixed.
// cmanske: NOT TRUE! We would like to pay attention to mixed state in Format
// | Align submenu!
// This routine assumes that alignment is done ONLY via divs
// Default alignment is left
*aMixed = false;
*aAlign = nsIHTMLEditor::eLeft;
// Get selection location
if (NS_WARN_IF(!HTMLEditorRef().GetRoot())) {
return NS_ERROR_FAILURE;
}
OwningNonNull<Element> root = *HTMLEditorRef().GetRoot();
int32_t rootOffset =
root->GetParentNode() ? root->GetParentNode()->ComputeIndexOf(root) : -1;
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
// Is the selection collapsed?
nsCOMPtr<nsINode> nodeToExamine;
if (SelectionRefPtr()->IsCollapsed() ||
atStartOfSelection.GetContainerAsText()) {
// If selection is collapsed, we want to look at the container of selection
// start and its ancestors for divs with alignment on them. If we are in a
// text node, then that is the node of interest.
nodeToExamine = atStartOfSelection.GetContainer();
if (NS_WARN_IF(!nodeToExamine)) {
return NS_ERROR_FAILURE;
}
} else if (atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::html) &&
atStartOfSelection.Offset() == static_cast<uint32_t>(rootOffset)) {
// If we have selected the body, let's look at the first editable node
nodeToExamine = HTMLEditorRef().GetNextEditableNode(atStartOfSelection);
if (NS_WARN_IF(!nodeToExamine)) {
return NS_ERROR_FAILURE;
}
} else {
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(arrayOfRanges, EditSubAction::eSetOrClearAlignment);
// Use these ranges to construct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
EditSubAction::eSetOrClearAlignment,
TouchContent::no);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nodeToExamine = arrayOfNodes.SafeElementAt(0);
if (NS_WARN_IF(!nodeToExamine)) {
return NS_ERROR_FAILURE;
}
}
RefPtr<Element> blockParent = HTMLEditorRef().GetBlock(*nodeToExamine);
if (NS_WARN_IF(!blockParent)) {
return NS_ERROR_FAILURE;
}
if (HTMLEditorRef().IsCSSEnabled() &&
CSSEditUtils::IsCSSEditableProperty(blockParent, nullptr,
nsGkAtoms::align)) {
// We are in CSS mode and we know how to align this element with CSS
nsAutoString value;
// Let's get the value(s) of text-align or margin-left/margin-right
CSSEditUtils::GetCSSEquivalentToHTMLInlineStyleSet(
blockParent, nullptr, nsGkAtoms::align, value, CSSEditUtils::eComputed);
if (value.EqualsLiteral("center") || value.EqualsLiteral("-moz-center") ||
value.EqualsLiteral("auto auto")) {
*aAlign = nsIHTMLEditor::eCenter;
return NS_OK;
}
if (value.EqualsLiteral("right") || value.EqualsLiteral("-moz-right") ||
value.EqualsLiteral("auto 0px")) {
*aAlign = nsIHTMLEditor::eRight;
return NS_OK;
}
if (value.EqualsLiteral("justify")) {
*aAlign = nsIHTMLEditor::eJustify;
return NS_OK;
}
*aAlign = nsIHTMLEditor::eLeft;
return NS_OK;
}
// Check up the ladder for divs with alignment
bool isFirstNodeToExamine = true;
for (; nodeToExamine; nodeToExamine = nodeToExamine->GetParentNode()) {
if (!isFirstNodeToExamine &&
nodeToExamine->IsHTMLElement(nsGkAtoms::table)) {
// The node to examine is a table and this is not the first node we
// examine; let's break here to materialize the 'inline-block' behaviour
// of html tables regarding to text alignment
return NS_OK;
}
if (CSSEditUtils::IsCSSEditableProperty(nodeToExamine, nullptr,
nsGkAtoms::align)) {
nsAutoString value;
CSSEditUtils::GetSpecifiedProperty(*nodeToExamine, *nsGkAtoms::textAlign,
value);
if (!value.IsEmpty()) {
if (value.EqualsLiteral("center")) {
*aAlign = nsIHTMLEditor::eCenter;
return NS_OK;
}
if (value.EqualsLiteral("right")) {
*aAlign = nsIHTMLEditor::eRight;
return NS_OK;
}
if (value.EqualsLiteral("justify")) {
*aAlign = nsIHTMLEditor::eJustify;
return NS_OK;
}
if (value.EqualsLiteral("left")) {
*aAlign = nsIHTMLEditor::eLeft;
return NS_OK;
}
// XXX
// text-align: start and end aren't supported yet
}
}
if (HTMLEditUtils::SupportsAlignAttr(*nodeToExamine)) {
// Check for alignment
nsAutoString typeAttrVal;
nodeToExamine->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::align,
typeAttrVal);
ToLowerCase(typeAttrVal);
if (!typeAttrVal.IsEmpty()) {
if (typeAttrVal.EqualsLiteral("center")) {
*aAlign = nsIHTMLEditor::eCenter;
} else if (typeAttrVal.EqualsLiteral("right")) {
*aAlign = nsIHTMLEditor::eRight;
} else if (typeAttrVal.EqualsLiteral("justify")) {
*aAlign = nsIHTMLEditor::eJustify;
} else {
*aAlign = nsIHTMLEditor::eLeft;
}
return NS_OK;
}
}
isFirstNodeToExamine = false;
}
return NS_OK;
}
static nsAtom& MarginPropertyAtomForIndent(nsINode& aNode) {
nsAutoString direction;
CSSEditUtils::GetComputedProperty(aNode, *nsGkAtoms::direction, direction);
return direction.EqualsLiteral("rtl") ? *nsGkAtoms::marginRight
: *nsGkAtoms::marginLeft;
}
nsresult HTMLEditRules::GetParagraphState(bool* aMixed, nsAString& outFormat) {
if (NS_WARN_IF(!aMixed)) {
return NS_ERROR_INVALID_ARG;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
// This routine is *heavily* tied to our ui choices in the paragraph
// style popup. I can't see a way around that.
*aMixed = true;
outFormat.Truncate(0);
AutoSafeEditorData setData(*this, *mHTMLEditor);
bool bMixed = false;
// using "x" as an uninitialized value, since "" is meaningful
nsAutoString formatStr(NS_LITERAL_STRING("x"));
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetParagraphFormatNodes(arrayOfNodes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// post process list. We need to replace any block nodes that are not format
// nodes with their content. This is so we only have to look "up" the
// hierarchy to find format nodes, instead of both up and down.
for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
auto& curNode = arrayOfNodes[i];
nsAutoString format;
// if it is a known format node we have it easy
if (IsBlockNode(curNode) && !HTMLEditUtils::IsFormatNode(curNode)) {
// arrayOfNodes.RemoveObject(curNode);
rv = AppendInnerFormatNodes(arrayOfNodes, curNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
// we might have an empty node list. if so, find selection parent
// and put that on the list
if (arrayOfNodes.IsEmpty()) {
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
arrayOfNodes.AppendElement(*selectionStartPoint.GetContainer());
}
// remember root node
Element* rootElement = HTMLEditorRef().GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_FAILURE;
}
// loop through the nodes in selection and examine their paragraph format
for (auto& curNode : Reversed(arrayOfNodes)) {
nsAutoString format;
// if it is a known format node we have it easy
if (HTMLEditUtils::IsFormatNode(curNode)) {
GetFormatString(curNode, format);
} else if (IsBlockNode(curNode)) {
// this is a div or some other non-format block.
// we should ignore it. Its children were appended to this list
// by AppendInnerFormatNodes() call above. We will get needed
// info when we examine them instead.
continue;
} else {
nsINode* node = curNode->GetParentNode();
while (node) {
if (node == rootElement) {
format.Truncate(0);
break;
} else if (HTMLEditUtils::IsFormatNode(node)) {
GetFormatString(node, format);
break;
}
// else keep looking up
node = node->GetParentNode();
}
}
// if this is the first node, we've found, remember it as the format
if (formatStr.EqualsLiteral("x")) {
formatStr = format;
}
// else make sure it matches previously found format
else if (format != formatStr) {
bMixed = true;
break;
}
}
*aMixed = bMixed;
outFormat = formatStr;
return NS_OK;
}
nsresult HTMLEditRules::AppendInnerFormatNodes(
nsTArray<OwningNonNull<nsINode>>& aArray, nsINode* aNode) {
MOZ_ASSERT(aNode);
// we only need to place any one inline inside this node onto
// the list. They are all the same for purposes of determining
// paragraph style. We use foundInline to track this as we are
// going through the children in the loop below.
bool foundInline = false;
for (nsIContent* child = aNode->GetFirstChild(); child;
child = child->GetNextSibling()) {
bool isBlock = IsBlockNode(*child);
bool isFormat = HTMLEditUtils::IsFormatNode(child);
if (isBlock && !isFormat) {
// if it's a div, etc., recurse
AppendInnerFormatNodes(aArray, child);
} else if (isFormat) {
aArray.AppendElement(*child);
} else if (!foundInline) {
// if this is the first inline we've found, use it
foundInline = true;
aArray.AppendElement(*child);
}
}
return NS_OK;
}
nsresult HTMLEditRules::GetFormatString(nsINode* aNode, nsAString& outFormat) {
NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
if (HTMLEditUtils::IsFormatNode(aNode)) {
aNode->NodeInfo()->NameAtom()->ToString(outFormat);
} else {
outFormat.Truncate();
}
return NS_OK;
}
nsresult HTMLEditRules::WillInsert(bool* aCancel) {
MOZ_ASSERT(IsEditorDataAvailable());
nsresult rv = TextEditRules::WillInsert(aCancel);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Adjust selection to prevent insertion after a moz-BR. This next only
// works for collapsed selections right now, because selection is a pain to
// work with when not collapsed. (no good way to extend start or end of
// selection), so we ignore those types of selections.
if (!SelectionRefPtr()->IsCollapsed()) {
return NS_OK;
}
// If we are after a mozBR in the same block, then move selection to be
// before it
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
// Get prior node
nsCOMPtr<nsIContent> priorNode =
HTMLEditorRef().GetPreviousEditableHTMLNode(atStartOfSelection);
if (priorNode && TextEditUtils::IsMozBR(priorNode)) {
RefPtr<Element> block1 =
HTMLEditorRef().GetBlock(*atStartOfSelection.GetContainer());
RefPtr<Element> block2 = HTMLEditorRef().GetBlockNodeParent(priorNode);
if (block1 && block1 == block2) {
// If we are here then the selection is right after a mozBR that is in
// the same block as the selection. We need to move the selection start
// to be before the mozBR.
EditorRawDOMPoint point(priorNode);
ErrorResult error;
SelectionRefPtr()->Collapse(point, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
}
if (mDidDeleteSelection &&
(mTopLevelEditSubAction == EditSubAction::eInsertText ||
mTopLevelEditSubAction == EditSubAction::eInsertTextComingFromIME ||
mTopLevelEditSubAction == EditSubAction::eDeleteSelectedContent)) {
nsresult rv = ReapplyCachedStyles();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// For most actions we want to clear the cached styles, but there are
// exceptions
if (!IsStyleCachePreservingSubAction(mTopLevelEditSubAction)) {
ClearCachedStyles();
}
return NS_OK;
}
nsresult HTMLEditRules::WillInsertText(EditSubAction aEditSubAction,
bool* aCancel, bool* aHandled,
const nsAString* inString,
nsAString* outString,
int32_t aMaxLength) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aCancel) || NS_WARN_IF(!aHandled)) {
return NS_ERROR_NULL_POINTER;
}
// initialize out param
*aCancel = false;
*aHandled = true;
// If the selection isn't collapsed, delete it. Don't delete existing inline
// tags, because we're hopefully going to insert text (bug 787432).
if (!SelectionRefPtr()->IsCollapsed()) {
nsresult rv = HTMLEditorRef().DeleteSelectionAsSubAction(
nsIEditor::eNone, nsIEditor::eNoStrip);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
// we need to get the doc
RefPtr<Document> doc = HTMLEditorRef().GetDocument();
if (NS_WARN_IF(!doc)) {
return NS_ERROR_FAILURE;
}
// for every property that is set, insert a new inline style node
rv = CreateStyleForInsertText(*doc);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// get the (collapsed) selection location
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint pointToInsert(firstRange->StartRef());
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(pointToInsert.IsSetAndValid());
// dont put text in places that can't have it
if (!EditorBase::IsTextNode(pointToInsert.GetContainer()) &&
!HTMLEditorRef().CanContainTag(*pointToInsert.GetContainer(),
*nsGkAtoms::textTagName)) {
return NS_ERROR_FAILURE;
}
if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
// Right now the WSRunObject code bails on empty strings, but IME needs
// the InsertTextWithTransaction() call to still happen since empty strings
// are meaningful there.
// If there is one or more IME selections, its minimum offset should be
// the insertion point.
int32_t IMESelectionOffset = HTMLEditorRef().GetIMESelectionStartOffsetIn(
pointToInsert.GetContainer());
if (IMESelectionOffset >= 0) {
pointToInsert.Set(pointToInsert.GetContainer(), IMESelectionOffset);
}
if (inString->IsEmpty()) {
rv = HTMLEditorRef().InsertTextWithTransaction(
*doc, *inString, EditorRawDOMPoint(pointToInsert));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
WSRunObject wsObj(&HTMLEditorRef(), pointToInsert);
rv = wsObj.InsertText(*doc, *inString, pointToInsert);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// aEditSubAction == kInsertText
// find where we are
EditorDOMPoint currentPoint(pointToInsert);
// is our text going to be PREformatted?
// We remember this so that we know how to handle tabs.
bool isPRE = EditorBase::IsPreformatted(pointToInsert.GetContainer());
// turn off the edit listener: we know how to
// build the "doc changed range" ourselves, and it's
// must faster to do it once here than to track all
// the changes one at a time.
AutoLockListener lockit(&mListenerEnabled);
// don't change my selection in subtransactions
AutoTransactionsConserveSelection dontChangeMySelection(HTMLEditorRef());
nsAutoString tString(*inString);
const char16_t* unicodeBuf = tString.get();
int32_t pos = 0;
NS_NAMED_LITERAL_STRING(newlineStr, LFSTR);
{
AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
&pointToInsert);
// for efficiency, break out the pre case separately. This is because
// its a lot cheaper to search the input string for only newlines than
// it is to search for both tabs and newlines.
if (isPRE || IsPlaintextEditor()) {
while (unicodeBuf && pos != -1 &&
pos < static_cast<int32_t>(inString->Length())) {
int32_t oldPos = pos;
int32_t subStrLen;
pos = tString.FindChar(nsCRT::LF, oldPos);
if (pos != -1) {
subStrLen = pos - oldPos;
// if first char is newline, then use just it
if (!subStrLen) {
subStrLen = 1;
}
} else {
subStrLen = tString.Length() - oldPos;
pos = tString.Length();
}
nsDependentSubstring subStr(tString, oldPos, subStrLen);
// is it a return?
if (subStr.Equals(newlineStr)) {
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(currentPoint,
nsIEditor::eNone);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
pos++;
if (brElement->GetNextSibling()) {
pointToInsert.Set(brElement->GetNextSibling());
} else {
pointToInsert.SetToEndOf(currentPoint.GetContainer());
}
// XXX In most cases, pointToInsert and currentPoint are same here.
// But if the <br> element has been moved to different point by
// mutation observer, those points become different.
currentPoint.Set(brElement);
DebugOnly<bool> advanced = currentPoint.AdvanceOffset();
NS_WARNING_ASSERTION(
advanced, "Failed to advance offset after the new <br> element");
NS_WARNING_ASSERTION(currentPoint == pointToInsert,
"Perhaps, <br> element position has been moved "
"to different point "
"by mutation observer");
} else {
EditorRawDOMPoint pointAfterInsertedString;
rv = HTMLEditorRef().InsertTextWithTransaction(
*doc, subStr, EditorRawDOMPoint(currentPoint),
&pointAfterInsertedString);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
currentPoint = pointAfterInsertedString;
pointToInsert = pointAfterInsertedString;
}
}
} else {
NS_NAMED_LITERAL_STRING(tabStr, "\t");
NS_NAMED_LITERAL_STRING(spacesStr, " ");
char specialChars[] = {TAB, nsCRT::LF, 0};
while (unicodeBuf && pos != -1 &&
pos < static_cast<int32_t>(inString->Length())) {
int32_t oldPos = pos;
int32_t subStrLen;
pos = tString.FindCharInSet(specialChars, oldPos);
if (pos != -1) {
subStrLen = pos - oldPos;
// if first char is newline, then use just it
if (!subStrLen) {
subStrLen = 1;
}
} else {
subStrLen = tString.Length() - oldPos;
pos = tString.Length();
}
nsDependentSubstring subStr(tString, oldPos, subStrLen);
WSRunObject wsObj(&HTMLEditorRef(), currentPoint);
// is it a tab?
if (subStr.Equals(tabStr)) {
EditorRawDOMPoint pointAfterInsertedSpaces;
rv = wsObj.InsertText(*doc, spacesStr, currentPoint,
&pointAfterInsertedSpaces);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
pos++;
currentPoint = pointAfterInsertedSpaces;
pointToInsert = pointAfterInsertedSpaces;
}
// is it a return?
else if (subStr.Equals(newlineStr)) {
RefPtr<Element> newBRElement = wsObj.InsertBreak(
*SelectionRefPtr(), currentPoint, nsIEditor::eNone);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!newBRElement)) {
return NS_ERROR_FAILURE;
}
pos++;
if (newBRElement->GetNextSibling()) {
pointToInsert.Set(newBRElement->GetNextSibling());
} else {
pointToInsert.SetToEndOf(currentPoint.GetContainer());
}
currentPoint.Set(newBRElement);
DebugOnly<bool> advanced = currentPoint.AdvanceOffset();
NS_WARNING_ASSERTION(
advanced, "Failed to advance offset to after the new <br> node");
// XXX If the newBRElement has been moved or removed by mutation
// observer, we hit this assert. We need to check if
// newBRElement is in expected point, though, we must have
// a lot of same bugs...
NS_WARNING_ASSERTION(
currentPoint == pointToInsert,
"Perhaps, newBRElement has been moved or removed unexpectedly");
} else {
EditorRawDOMPoint pointAfterInsertedString;
rv = wsObj.InsertText(*doc, subStr, currentPoint,
&pointAfterInsertedString);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
currentPoint = pointAfterInsertedString;
pointToInsert = pointAfterInsertedString;
}
}
}
// After this block, pointToInsert is updated by AutoTrackDOMPoint.
}
IgnoredErrorResult ignoredError;
SelectionRefPtr()->SetInterlinePosition(false, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to unset interline position");
if (currentPoint.IsSet()) {
IgnoredErrorResult ignoredError;
SelectionRefPtr()->Collapse(currentPoint, ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to collapse at current point");
}
// manually update the doc changed range so that AfterEdit will clean up
// the correct portion of the document.
if (!mDocChangeRange) {
mDocChangeRange = new nsRange(pointToInsert.GetContainer());
}
if (currentPoint.IsSet()) {
rv = mDocChangeRange->SetStartAndEnd(pointToInsert, currentPoint);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
rv = mDocChangeRange->CollapseTo(pointToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::WillLoadHTML() {
MOZ_ASSERT(IsEditorDataAvailable());
// Delete mBogusNode if it exists. If we really need one,
// it will be added during post-processing in AfterEditInner().
if (mBogusNode) {
DebugOnly<nsresult> rv =
HTMLEditorRef().DeleteNodeWithTransaction(*mBogusNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove the bogus node");
mBogusNode = nullptr;
}
return NS_OK;
}
bool HTMLEditRules::CanContainParagraph(Element& aElement) const {
MOZ_ASSERT(IsEditorDataAvailable());
if (HTMLEditorRef().CanContainTag(aElement, *nsGkAtoms::p)) {
return true;
}
// Even if the element cannot have a <p> element as a child, it can contain
// <p> element as a descendant if it's one of the following elements.
if (aElement.IsAnyOfHTMLElements(nsGkAtoms::ol, nsGkAtoms::ul, nsGkAtoms::dl,
nsGkAtoms::table, nsGkAtoms::thead,
nsGkAtoms::tbody, nsGkAtoms::tfoot,
nsGkAtoms::tr)) {
return true;
}
// XXX Otherwise, Chromium checks the CSS box is a block, but we don't do it
// for now.
return false;
}
EditActionResult HTMLEditRules::WillInsertParagraphSeparator() {
MOZ_ASSERT(IsEditorDataAvailable());
// If the selection isn't collapsed, delete it.
if (!SelectionRefPtr()->IsCollapsed()) {
nsresult rv = HTMLEditorRef().DeleteSelectionAsSubAction(nsIEditor::eNone,
nsIEditor::eStrip);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
}
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
// Split any mailcites in the way. Should we abort this if we encounter
// table cell boundaries?
if (IsMailEditor()) {
EditActionResult result = SplitMailCites();
if (NS_WARN_IF(result.Failed())) {
return result;
}
if (result.Handled()) {
return result;
}
}
// Smart splitting rules
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
EditorDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
// Do nothing if the node is read-only
if (!HTMLEditorRef().IsModifiableNode(*atStartOfSelection.GetContainer())) {
return EditActionCanceled();
}
// If the active editing host is an inline element, or if the active editing
// host is the block parent itself and we're configured to use <br> as a
// paragraph separator, just append a <br>.
RefPtr<Element> host = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!host)) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
// Look for the nearest parent block. However, don't return error even if
// there is no block parent here because in such case, i.e., editing host
// is an inline element, we should insert <br> simply.
RefPtr<Element> blockParent =
HTMLEditor::GetBlock(*atStartOfSelection.GetContainer(), host);
ParagraphSeparator separator = HTMLEditorRef().GetDefaultParagraphSeparator();
bool insertBRElement;
// If there is no block parent in the editing host, i.e., the editing host
// itself is also a non-block element, we should insert a <br> element.
if (!blockParent) {
// XXX Chromium checks if the CSS box of the editing host is block.
insertBRElement = true;
}
// If only the editing host is block, and the default paragraph separator
// is <br> or the editing host cannot contain a <p> element, we should
// insert a <br> element.
else if (host == blockParent) {
insertBRElement =
separator == ParagraphSeparator::br || !CanContainParagraph(*host);
}
// If the nearest block parent is a single-line container declared in
// the execCommand spec and not the editing host, we should separate the
// block even if the default paragraph separator is <br> element.
else if (HTMLEditUtils::IsSingleLineContainer(*blockParent)) {
insertBRElement = false;
}
// Otherwise, unless there is no block ancestor which can contain <p>
// element, we shouldn't insert a <br> element here.
else {
insertBRElement = true;
for (Element* blockAncestor = blockParent; blockAncestor && insertBRElement;
blockAncestor = HTMLEditor::GetBlockNodeParent(blockAncestor, host)) {
insertBRElement = !CanContainParagraph(*blockAncestor);
}
}
// If we cannot insert a <p>/<div> element at the selection, we should insert
// a <br> element instead.
if (insertBRElement) {
nsresult rv = InsertBRElement(atStartOfSelection);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
return EditActionHandled();
}
if (host == blockParent && separator != ParagraphSeparator::br) {
// Insert a new block first
MOZ_ASSERT(separator == ParagraphSeparator::div ||
separator == ParagraphSeparator::p);
// MakeBasicBlock() creates AutoSelectionRestorer.
// Therefore, even if it returns NS_OK, editor might have been destroyed
// at restoring Selection.
nsresult rv = MakeBasicBlock(ParagraphSeparatorElement(separator));
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) ||
NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
// We warn on failure, but don't handle it, because it might be harmless.
// Instead we just check that a new block was actually created.
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"HTMLEditRules::MakeBasicBlock() failed");
firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
atStartOfSelection = firstRange->StartRef();
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
blockParent =
HTMLEditor::GetBlock(*atStartOfSelection.GetContainer(), host);
if (NS_WARN_IF(!blockParent)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
if (NS_WARN_IF(blockParent == host)) {
// Didn't create a new block for some reason, fall back to <br>
rv = InsertBRElement(atStartOfSelection);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
return EditActionHandled();
}
// Now, mNewBlock is last created block element for wrapping inline
// elements around the caret position and AfterEditInner() will move
// caret into it. However, it may be different from block parent of
// the caret position. E.g., MakeBasicBlock() may wrap following
// inline elements of a <br> element which is next sibling of container
// of the caret. So, we need to adjust mNewBlock here for avoiding
// jumping caret to odd position.
mNewBlock = blockParent;
}
// If block is empty, populate with br. (For example, imagine a div that
// contains the word "text". The user selects "text" and types return.
// "Text" is deleted leaving an empty block. We want to put in one br to
// make block have a line. Then code further below will put in a second br.)
if (IsEmptyBlockElement(*blockParent, IgnoreSingleBR::eNo)) {
AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection);
EditorRawDOMPoint endOfBlockParent;
endOfBlockParent.SetToEndOf(blockParent);
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(endOfBlockParent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(!brElement)) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
}
nsCOMPtr<Element> listItem = IsInListItem(blockParent);
if (listItem && listItem != host) {
nsresult rv =
ReturnInListItem(*listItem, *atStartOfSelection.GetContainer(),
atStartOfSelection.Offset());
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to insert break into list item");
return EditActionHandled();
}
if (HTMLEditUtils::IsHeader(*blockParent)) {
// Headers: close (or split) header
nsresult rv =
ReturnInHeader(*blockParent, *atStartOfSelection.GetContainer(),
atStartOfSelection.Offset());
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to handle insertParagraph in the heading element");
return EditActionHandled();
}
// XXX Ideally, we should take same behavior with both <p> container and
// <div> container. However, we are still using <br> as default
// paragraph separator (non-standard) and we've split only <p> container
// long time. Therefore, some web apps may depend on this behavior like
// Gmail. So, let's use traditional odd behavior only when the default
// paragraph separator is <br>. Otherwise, take consistent behavior
// between <p> container and <div> container.
if ((separator == ParagraphSeparator::br &&
blockParent->IsHTMLElement(nsGkAtoms::p)) ||
(separator != ParagraphSeparator::br &&
blockParent->IsAnyOfHTMLElements(nsGkAtoms::p, nsGkAtoms::div))) {
AutoEditorDOMPointChildInvalidator lockOffset(atStartOfSelection);
// Paragraphs: special rules to look for <br>s
EditActionResult result = ReturnInParagraph(*blockParent);
if (NS_WARN_IF(result.Failed())) {
return result;
}
if (result.Handled()) {
// Now, atStartOfSelection may be invalid because the left paragraph
// may have less children than its offset. For avoiding warnings of
// validation of EditorDOMPoint, we should not touch it anymore.
lockOffset.Cancel();
return result;
}
// Fall through, if ReturnInParagraph() didn't handle it.
MOZ_ASSERT(!result.Canceled(),
"ReturnInParagraph canceled this edit action, "
"WillInsertBreak() needs to handle such case");
}
// If nobody handles this edit action, let's insert new <br> at the selection.
rv = InsertBRElement(atStartOfSelection);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
return EditActionHandled();
}
nsresult HTMLEditRules::InsertBRElement(const EditorDOMPoint& aPointToBreak) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aPointToBreak.IsSet())) {
return NS_ERROR_INVALID_ARG;
}
bool brElementIsAfterBlock = false;
bool brElementIsBeforeBlock = false;
// First, insert a <br> element.
RefPtr<Element> brElement;
if (IsPlaintextEditor()) {
brElement = HTMLEditorRef().InsertBrElementWithTransaction(aPointToBreak);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
} else {
EditorDOMPoint pointToBreak(aPointToBreak);
WSRunObject wsObj(&HTMLEditorRef(), pointToBreak);
WSType wsType;
wsObj.PriorVisibleNode(pointToBreak, &wsType);
if (wsType & WSType::block) {
brElementIsAfterBlock = true;
}
wsObj.NextVisibleNode(pointToBreak, &wsType);
if (wsType & WSType::block) {
brElementIsBeforeBlock = true;
}
// If the container of the break is a link, we need to split it and
// insert new <br> between the split links.
RefPtr<Element> linkNode =
HTMLEditor::GetLinkElement(pointToBreak.GetContainer());
if (linkNode) {
SplitNodeResult splitLinkNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*linkNode, pointToBreak,
SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitLinkNodeResult.Failed())) {
return splitLinkNodeResult.Rv();
}
pointToBreak = splitLinkNodeResult.SplitPoint();
}
brElement =
wsObj.InsertBreak(*SelectionRefPtr(), pointToBreak, nsIEditor::eNone);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
}
// If the <br> element has already been removed from the DOM tree by a
// mutation observer, don't continue handling this.
if (NS_WARN_IF(!brElement->GetParentNode())) {
return NS_ERROR_FAILURE;
}
if (brElementIsAfterBlock && brElementIsBeforeBlock) {
// We just placed a <br> between block boundaries. This is the one case
// where we want the selection to be before the br we just placed, as the
// br will be on a new line, rather than at end of prior line.
// XXX brElementIsAfterBlock and brElementIsBeforeBlock were set before
// modifying the DOM tree. So, now, the <br> element may not be
// between blocks.
ErrorResult error;
SelectionRefPtr()->SetInterlinePosition(true, error);
NS_WARNING_ASSERTION(!error.Failed(), "Failed to set interline position");
EditorRawDOMPoint point(brElement);
error = NS_OK;
SelectionRefPtr()->Collapse(point, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
EditorDOMPoint afterBRElement(brElement);
DebugOnly<bool> advanced = afterBRElement.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after the new <br> element");
WSRunObject wsObj(&HTMLEditorRef(), afterBRElement);
nsCOMPtr<nsINode> maybeSecondBRNode;
WSType wsType;
wsObj.NextVisibleNode(afterBRElement, address_of(maybeSecondBRNode), nullptr,
&wsType);
if (wsType == WSType::br) {
// The next thing after the break we inserted is another break. Move the
// second break to be the first break's sibling. This will prevent them
// from being in different inline nodes, which would break
// SetInterlinePosition(). It will also assure that if the user clicks
// away and then clicks back on their new blank line, they will still get
// the style from the line above.
EditorDOMPoint atSecondBRElement(maybeSecondBRNode);
if (brElement->GetNextSibling() != maybeSecondBRNode) {
nsresult rv = HTMLEditorRef().MoveNodeWithTransaction(
*maybeSecondBRNode->AsContent(), afterBRElement);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
// SetInterlinePosition(true) means we want the caret to stick to the
// content on the "right". We want the caret to stick to whatever is past
// the break. This is because the break is on the same line we were on,
// but the next content will be on the following line.
// An exception to this is if the break has a next sibling that is a block
// node. Then we stick to the left to avoid an uber caret.
nsIContent* nextSiblingOfBRElement = brElement->GetNextSibling();
ErrorResult error;
SelectionRefPtr()->SetInterlinePosition(
!(nextSiblingOfBRElement && IsBlockNode(*nextSiblingOfBRElement)), error);
NS_WARNING_ASSERTION(!error.Failed(),
"Failed to set or unset interline position");
error = NS_OK;
SelectionRefPtr()->Collapse(afterBRElement, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
EditActionResult HTMLEditRules::SplitMailCites() {
MOZ_ASSERT(IsEditorDataAvailable());
EditorRawDOMPoint pointToSplit(EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!pointToSplit.IsSet())) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
RefPtr<Element> citeNode =
GetTopEnclosingMailCite(*pointToSplit.GetContainer());
if (!citeNode) {
return EditActionIgnored();
}
// If our selection is just before a break, nudge it to be just after it.
// This does two things for us. It saves us the trouble of having to add
// a break here ourselves to preserve the "blockness" of the inline span
// mailquote (in the inline case), and :
// it means the break won't end up making an empty line that happens to be
// inside a mailquote (in either inline or block case).
// The latter can confuse a user if they click there and start typing,
// because being in the mailquote may affect wrapping behavior, or font
// color, etc.
WSRunObject wsObj(&HTMLEditorRef(), pointToSplit);
nsCOMPtr<nsINode> visNode;
WSType wsType;
wsObj.NextVisibleNode(pointToSplit, address_of(visNode), nullptr, &wsType);
// If selection start point is before a break and it's inside the mailquote,
// let's split it after the visible node.
if (wsType == WSType::br && visNode != citeNode &&
citeNode->Contains(visNode)) {
pointToSplit.Set(visNode);
DebugOnly<bool> advanced = pointToSplit.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to after the visible node");
}
if (NS_WARN_IF(!pointToSplit.GetContainerAsContent())) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
SplitNodeResult splitCiteNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*citeNode, pointToSplit, SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(splitCiteNodeResult.Failed())) {
return EditActionIgnored(splitCiteNodeResult.Rv());
}
pointToSplit.Clear();
// Add an invisible <br> to the end of current cite node (If new left cite
// has not been created, we're at the end of it. Otherwise, we're still at
// the right node) if it was a <span> of style="display: block". This is
// important, since when serializing the cite to plain text, the span which
// caused the visual break is discarded. So the added <br> will guarantee
// that the serializer will insert a break where the user saw one.
// FYI: splitCiteNodeResult grabs the previous node with nsCOMPtr. So, it's
// safe to access previousNodeOfSplitPoint even after changing the DOM
// tree and/or selection even though it's raw pointer.
nsIContent* previousNodeOfSplitPoint = splitCiteNodeResult.GetPreviousNode();
if (previousNodeOfSplitPoint &&
previousNodeOfSplitPoint->IsHTMLElement(nsGkAtoms::span) &&
previousNodeOfSplitPoint->GetPrimaryFrame() &&
previousNodeOfSplitPoint->GetPrimaryFrame()->IsBlockFrameOrSubclass()) {
nsCOMPtr<nsINode> lastChild = previousNodeOfSplitPoint->GetLastChild();
if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
// We ignore the result here.
EditorRawDOMPoint endOfPreviousNodeOfSplitPoint;
endOfPreviousNodeOfSplitPoint.SetToEndOf(previousNodeOfSplitPoint);
RefPtr<Element> invisibleBrElement =
HTMLEditorRef().InsertBrElementWithTransaction(
endOfPreviousNodeOfSplitPoint);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(invisibleBrElement,
"Failed to create an invisible <br> element");
}
}
// In most cases, <br> should be inserted after current cite. However, if
// left cite hasn't been created because the split point was start of the
// cite node, <br> should be inserted before the current cite.
EditorRawDOMPoint pointToInsertBrNode(splitCiteNodeResult.SplitPoint());
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(pointToInsertBrNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(!brElement)) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
// Now, offset of pointToInsertBrNode is invalid. Let's clear it.
pointToInsertBrNode.Clear();
// Want selection before the break, and on same line.
EditorDOMPoint atBrNode(brElement);
Unused << atBrNode.Offset(); // Needs offset after collapsing the selection.
ErrorResult error;
SelectionRefPtr()->SetInterlinePosition(true, error);
NS_WARNING_ASSERTION(!error.Failed(), "Failed to set interline position");
error = NS_OK;
SelectionRefPtr()->Collapse(atBrNode, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(error.Failed())) {
return EditActionIgnored(error.StealNSResult());
}
// if citeNode wasn't a block, we might also want another break before it.
// We need to examine the content both before the br we just added and also
// just after it. If we don't have another br or block boundary adjacent,
// then we will need a 2nd br added to achieve blank line that user expects.
if (IsInlineNode(*citeNode)) {
// Use DOM point which we tried to collapse to.
EditorRawDOMPoint pointToCreateNewBrNode(atBrNode.GetContainer(),
atBrNode.Offset());
WSRunObject wsObj(&HTMLEditorRef(), pointToCreateNewBrNode);
WSType wsType;
wsObj.PriorVisibleNode(pointToCreateNewBrNode, nullptr, nullptr, &wsType);
if (wsType == WSType::normalWS || wsType == WSType::text ||
wsType == WSType::special) {
EditorRawDOMPoint pointAfterNewBrNode(pointToCreateNewBrNode);
DebugOnly<bool> advanced = pointAfterNewBrNode.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after the <br> node");
WSRunObject wsObjAfterBR(&HTMLEditorRef(), pointAfterNewBrNode);
wsObjAfterBR.NextVisibleNode(pointAfterNewBrNode, &wsType);
if (wsType == WSType::normalWS || wsType == WSType::text ||
wsType == WSType::special ||
// In case we're at the very end.
wsType == WSType::thisBlock) {
brElement = HTMLEditorRef().InsertBrElementWithTransaction(
pointToCreateNewBrNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(!brElement)) {
return EditActionIgnored(NS_ERROR_FAILURE);
}
// Now, those points may be invalid.
pointToCreateNewBrNode.Clear();
pointAfterNewBrNode.Clear();
}
}
}
// delete any empty cites
bool bEmptyCite = false;
if (previousNodeOfSplitPoint) {
nsresult rv = HTMLEditorRef().IsEmptyNode(previousNodeOfSplitPoint,
&bEmptyCite, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
if (bEmptyCite) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*previousNodeOfSplitPoint);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
}
}
if (citeNode) {
nsresult rv =
HTMLEditorRef().IsEmptyNode(citeNode, &bEmptyCite, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
if (bEmptyCite) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*citeNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
}
}
return EditActionHandled();
}
nsresult HTMLEditRules::WillDeleteSelection(
nsIEditor::EDirection aAction, nsIEditor::EStripWrappers aStripWrappers,
bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
aStripWrappers == nsIEditor::eNoStrip);
if (NS_WARN_IF(!aCancel) || NS_WARN_IF(!aHandled)) {
return NS_ERROR_INVALID_ARG;
}
// Initialize out params
*aCancel = false;
*aHandled = false;
// Remember that we did a selection deletion. Used by
// CreateStyleForInsertText()
mDidDeleteSelection = true;
// If there is only bogus content, cancel the operation
if (mBogusNode) {
*aCancel = true;
return NS_OK;
}
// First check for table selection mode. If so, hand off to table editor.
ErrorResult error;
RefPtr<Element> cellElement =
HTMLEditorRef().GetFirstSelectedTableCellElement(error);
if (cellElement) {
error.SuppressException();
nsresult rv = HTMLEditorRef().DeleteTableCellContentsWithTransaction();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
*aHandled = true;
return rv;
}
nsresult rv = error.StealNSResult();
cellElement = nullptr;
// origCollapsed is used later to determine whether we should join blocks. We
// don't really care about bCollapsed because it will be modified by
// ExtendSelectionForDelete later. TryToJoinBlocksWithTransaction() should
// happen if the original selection is collapsed and the cursor is at the end
// of a block element, in which case ExtendSelectionForDelete would always
// make the selection not collapsed.
bool join = false;
bool origCollapsed = SelectionRefPtr()->IsCollapsed();
if (origCollapsed) {
EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!startPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// If we are inside an empty block, delete it.
RefPtr<Element> host = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!host)) {
return NS_ERROR_FAILURE;
}
{
AutoEditorDOMPointChildInvalidator lockOffset(startPoint);
rv = MaybeDeleteTopMostEmptyAncestor(*startPoint.GetContainer(), *host,
aAction, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (*aHandled) {
return NS_OK;
}
}
// Test for distance between caret and text that will be deleted
rv = CheckBidiLevelForDeletion(startPoint, aAction, aCancel);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (*aCancel) {
return NS_OK;
}
// ExtendSelectionForDelete will use selection controller to set
// selection for delete. But if focus event doesn't receive yet,
// ancestor isn't set. So we must set root eleement of editor to
// ancestor.
AutoSetTemporaryAncestorLimiter autoSetter(
HTMLEditorRef(), *SelectionRefPtr(), *startPoint.GetContainer());
rv = HTMLEditorRef().ExtendSelectionForDelete(&aAction);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We should delete nothing.
if (aAction == nsIEditor::eNone) {
return NS_OK;
}
}
if (SelectionRefPtr()->IsCollapsed()) {
// ExtendSelectionForDelete() won't change the selection.
EditorDOMPoint startPoint(EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!startPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// What's in the direction we are deleting?
WSRunObject wsObj(&HTMLEditorRef(), startPoint);
nsCOMPtr<nsINode> visNode;
int32_t visOffset;
WSType wsType;
// Find next visible node
if (aAction == nsIEditor::eNext) {
wsObj.NextVisibleNode(startPoint, address_of(visNode), &visOffset,
&wsType);
} else {
wsObj.PriorVisibleNode(startPoint, address_of(visNode), &visOffset,
&wsType);
}
if (!visNode) {
// Can't find anything to delete!
*aCancel = true;
// XXX This is the result of
// HTMLEditorRef().GetFirstSelectedTableCellElement().
// The value could be both an error and NS_OK.
return rv;
}
if (wsType == WSType::normalWS) {
// We found some visible ws to delete. Let ws code handle it.
*aHandled = true;
if (aAction == nsIEditor::eNext) {
rv = wsObj.DeleteWSForward();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = wsObj.DeleteWSBackward();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
rv = InsertBRIfNeeded();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (wsType == WSType::text) {
// Found normal text to delete.
OwningNonNull<Text> nodeAsText = *visNode->GetAsText();
int32_t so = visOffset;
int32_t eo = visOffset + 1;
if (aAction == nsIEditor::ePrevious) {
if (!so) {
return NS_ERROR_UNEXPECTED;
}
so--;
eo--;
// Bug 1068979: delete both codepoints if surrogate pair
if (so > 0) {
const nsTextFragment* text = nodeAsText->GetText();
if (NS_IS_LOW_SURROGATE(text->CharAt(so)) &&
NS_IS_HIGH_SURROGATE(text->CharAt(so - 1))) {
so--;
}
}
} else {
RefPtr<nsRange> range = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!range)) {
return NS_ERROR_FAILURE;
}
NS_ASSERTION(range->GetStartContainer() == visNode,
"selection start not in visNode");
NS_ASSERTION(range->GetEndContainer() == visNode,
"selection end not in visNode");
so = range->StartOffset();
eo = range->EndOffset();
}
rv = WSRunObject::PrepareToDeleteRange(
&HTMLEditorRef(), address_of(visNode), &so, address_of(visNode), &eo);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
*aHandled = true;
rv = HTMLEditorRef().DeleteTextWithTransaction(
nodeAsText, std::min(so, eo), DeprecatedAbs(eo - so));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// XXX When Backspace key is pressed, Chromium removes following empty
// text nodes when removing the last character of the non-empty text
// node. However, Edge never removes empty text nodes even if
// selection is in the following empty text node(s). For now, we
// should keep our traditional behavior same as Edge for backward
// compatibility.
// XXX When Delete key is pressed, Edge removes all preceding empty
// text nodes when removing the first character of the non-empty
// text node. Chromium removes only selected empty text node and
// following empty text nodes and the first character of the
// non-empty text node. For now, we should keep our traditional
// behavior same as Chromium for backward compatibility.
rv = DeleteNodeIfCollapsedText(nodeAsText);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove collapsed text");
rv = InsertBRIfNeeded();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Remember that we did a ranged delete for the benefit of
// AfterEditInner().
mDidRangedDelete = true;
return NS_OK;
}
if (wsType == WSType::special || wsType == WSType::br ||
visNode->IsHTMLElement(nsGkAtoms::hr)) {
// Short circuit for invisible breaks. delete them and recurse.
if (visNode->IsHTMLElement(nsGkAtoms::br) &&
!HTMLEditorRef().IsVisibleBRElement(visNode)) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*visNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = WillDeleteSelection(aAction, aStripWrappers, aCancel, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// Special handling for backspace when positioned after <hr>
if (aAction == nsIEditor::ePrevious &&
visNode->IsHTMLElement(nsGkAtoms::hr)) {
// Only if the caret is positioned at the end-of-hr-line position, we
// want to delete the <hr>.
//
// In other words, we only want to delete, if our selection position
// (indicated by startPoint) is the position directly
// after the <hr>, on the same line as the <hr>.
//
// To detect this case we check:
// startPoint's container == parentOfVisNode
// and
// startPoint's offset -1 == visNodeOffsetToVisNodeParent
// and
// interline position is false (left)
//
// In any other case we set the position to startPoint's container -1
// and interlineposition to false, only moving the caret to the
// end-of-hr-line position.
bool moveOnly = true;
EditorDOMPoint selPoint(visNode);
ErrorResult err;
bool interLineIsRight = SelectionRefPtr()->GetInterlinePosition(err);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}
if (startPoint.GetContainer() == selPoint.GetContainer() &&
startPoint.Offset() - 1 == selPoint.Offset() && !interLineIsRight) {
moveOnly = false;
}
if (moveOnly) {
// Go to the position after the <hr>, but to the end of the <hr> line
// by setting the interline position to left.
DebugOnly<bool> advanced = selPoint.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after <hr> element");
{
AutoEditorDOMPointChildInvalidator lockOffset(selPoint);
IgnoredErrorResult ignoredError;
SelectionRefPtr()->Collapse(selPoint, ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!ignoredError.Failed(),
"Failed to collapse selection at after the <hr>");
}
IgnoredErrorResult ignoredError;
SelectionRefPtr()->SetInterlinePosition(false, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to unset interline position");
mDidExplicitlySetInterline = true;
*aHandled = true;
// There is one exception to the move only case. If the <hr> is
// followed by a <br> we want to delete the <br>.
WSType otherWSType;
nsCOMPtr<nsINode> otherNode;
wsObj.NextVisibleNode(startPoint, address_of(otherNode), nullptr,
&otherWSType);
if (otherWSType == WSType::br) {
// Delete the <br>
if (NS_WARN_IF(!otherNode->IsContent())) {
return NS_ERROR_FAILURE;
}
nsIContent* otherContent = otherNode->AsContent();
rv = WSRunObject::PrepareToDeleteNode(&HTMLEditorRef(),
otherContent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = HTMLEditorRef().DeleteNodeWithTransaction(*otherContent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
// Else continue with normal delete code
}
if (NS_WARN_IF(!visNode->IsContent())) {
return NS_ERROR_FAILURE;
}
// Found break or image, or hr.
rv = WSRunObject::PrepareToDeleteNode(&HTMLEditorRef(),
visNode->AsContent());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Remember sibling to visnode, if any
nsCOMPtr<nsIContent> sibling =
HTMLEditorRef().GetPriorHTMLSibling(visNode);
// Delete the node, and join like nodes if appropriate
rv = HTMLEditorRef().DeleteNodeWithTransaction(*visNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We did something, so let's say so.
*aHandled = true;
// Is there a prior node and are they siblings?
nsCOMPtr<nsINode> stepbrother;
if (sibling) {
stepbrother = HTMLEditorRef().GetNextHTMLSibling(sibling);
}
// Are they both text nodes? If so, join them!
if (startPoint.GetContainer() == stepbrother &&
startPoint.GetContainerAsText() && sibling->GetAsText()) {
EditorDOMPoint pt;
nsresult rv = JoinNearestEditableNodesWithTransaction(
*sibling, *startPoint.GetContainerAsContent(), &pt);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (NS_WARN_IF(!pt.IsSet())) {
return NS_ERROR_FAILURE;
}
// Fix up selection
ErrorResult error;
SelectionRefPtr()->Collapse(pt, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
rv = InsertBRIfNeeded();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (wsType == WSType::otherBlock) {
// Make sure it's not a table element. If so, cancel the operation
// (translation: users cannot backspace or delete across table cells)
if (HTMLEditUtils::IsTableElement(visNode)) {
*aCancel = true;
return NS_OK;
}
// Next to a block. See if we are between a block and a br. If so, we
// really want to delete the br. Else join content at selection to the
// block.
bool bDeletedBR = false;
WSType otherWSType;
nsCOMPtr<nsINode> otherNode;
// Find node in other direction
if (aAction == nsIEditor::eNext) {
wsObj.PriorVisibleNode(startPoint, address_of(otherNode), nullptr,
&otherWSType);
} else {
wsObj.NextVisibleNode(startPoint, address_of(otherNode), nullptr,
&otherWSType);
}
// First find the adjacent node in the block
nsCOMPtr<nsIContent> leafNode;
nsCOMPtr<nsINode> leftNode, rightNode;
if (aAction == nsIEditor::ePrevious) {
leafNode = HTMLEditorRef().GetLastEditableLeaf(*visNode);
leftNode = leafNode;
rightNode = startPoint.GetContainer();
} else {
leafNode = HTMLEditorRef().GetFirstEditableLeaf(*visNode);
leftNode = startPoint.GetContainer();
rightNode = leafNode;
}
if (otherNode->IsHTMLElement(nsGkAtoms::br)) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*otherNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// XXX Only in this case, setting "handled" to true only when it
// succeeds?
*aHandled = true;
bDeletedBR = true;
}
// Don't cross table boundaries
if (leftNode && rightNode &&
InDifferentTableElements(leftNode, rightNode)) {
return NS_OK;
}
if (bDeletedBR) {
// Put selection at edge of block and we are done.
if (NS_WARN_IF(!leafNode)) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint newSel = GetGoodSelPointForNode(*leafNode, aAction);
if (NS_WARN_IF(!newSel.IsSet())) {
return NS_ERROR_FAILURE;
}
IgnoredErrorResult error;
SelectionRefPtr()->Collapse(newSel, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!error.Failed(),
"Failed to collapse selection at edge of the block");
return NS_OK;
}
// Else we are joining content to block
EditorDOMPoint selPoint(startPoint);
{
AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(), &selPoint);
if (NS_WARN_IF(!leftNode) || NS_WARN_IF(!leftNode->IsContent()) ||
NS_WARN_IF(!rightNode) || NS_WARN_IF(!rightNode->IsContent())) {
return NS_ERROR_FAILURE;
}
EditActionResult ret = TryToJoinBlocksWithTransaction(
*leftNode->AsContent(), *rightNode->AsContent());
*aHandled |= ret.Handled();
*aCancel |= ret.Canceled();
if (NS_WARN_IF(ret.Failed())) {
return ret.Rv();
}
}
// If TryToJoinBlocksWithTransaction() didn't handle it and it's not
// canceled, user may want to modify the start leaf node or the last leaf
// node of the block.
if (!*aHandled && !*aCancel && leafNode != startPoint.GetContainer()) {
int32_t offset = aAction == nsIEditor::ePrevious
? static_cast<int32_t>(leafNode->Length())
: 0;
rv = SelectionRefPtr()->Collapse(leafNode, offset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to collapse selection at the leaf node");
rv = WillDeleteSelection(aAction, aStripWrappers, aCancel, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// Otherwise, we must have deleted the selection as user expected.
IgnoredErrorResult ignored;
SelectionRefPtr()->Collapse(selPoint, ignored);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignored.Failed(),
"Failed to selection at deleted point");
return NS_OK;
}
if (wsType == WSType::thisBlock) {
// At edge of our block. Look beside it and see if we can join to an
// adjacent block
// Make sure it's not a table element. If so, cancel the operation
// (translation: users cannot backspace or delete across table cells)
if (HTMLEditUtils::IsTableElement(visNode)) {
*aCancel = true;
return NS_OK;
}
// First find the relevant nodes
nsCOMPtr<nsINode> leftNode, rightNode;
if (aAction == nsIEditor::ePrevious) {
leftNode = HTMLEditorRef().GetPreviousEditableHTMLNode(*visNode);
rightNode = startPoint.GetContainer();
} else {
rightNode = HTMLEditorRef().GetNextEditableHTMLNode(*visNode);
leftNode = startPoint.GetContainer();
}
// Nothing to join
if (!leftNode || !rightNode) {
*aCancel = true;
return NS_OK;
}
// Don't cross table boundaries -- cancel it
if (InDifferentTableElements(leftNode, rightNode)) {
*aCancel = true;
return NS_OK;
}
EditorDOMPoint selPoint(startPoint);
{
AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(), &selPoint);
if (NS_WARN_IF(!leftNode->IsContent()) ||
NS_WARN_IF(!rightNode->IsContent())) {
return NS_ERROR_FAILURE;
}
EditActionResult ret = TryToJoinBlocksWithTransaction(
*leftNode->AsContent(), *rightNode->AsContent());
// This should claim that trying to join the block means that
// this handles the action because the caller shouldn't do anything
// anymore in this case.
*aHandled = true;
*aCancel |= ret.Canceled();
if (NS_WARN_IF(ret.Failed())) {
return ret.Rv();
}
}
IgnoredErrorResult ignored;
SelectionRefPtr()->Collapse(selPoint, ignored);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignored.Failed(), "Failed to collapse selection");
return NS_OK;
}
}
// Else we have a non-collapsed selection. First adjust the selection.
rv = ExpandSelectionForDeletion();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Remember that we did a ranged delete for the benefit of AfterEditInner().
mDidRangedDelete = true;
// Refresh start and end points
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsINode> startNode = firstRange->GetStartContainer();
if (NS_WARN_IF(!startNode)) {
return NS_ERROR_FAILURE;
}
int32_t startOffset = firstRange->StartOffset();
nsCOMPtr<nsINode> endNode = firstRange->GetEndContainer();
if (NS_WARN_IF(!endNode)) {
return NS_ERROR_FAILURE;
}
int32_t endOffset = firstRange->EndOffset();
// Figure out if the endpoints are in nodes that can be merged. Adjust
// surrounding whitespace in preparation to delete selection.
if (!IsPlaintextEditor()) {
AutoTransactionsConserveSelection dontChangeMySelection(HTMLEditorRef());
rv = WSRunObject::PrepareToDeleteRange(&HTMLEditorRef(),
address_of(startNode), &startOffset,
address_of(endNode), &endOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
{
// Track location of where we are deleting
AutoTrackDOMPoint startTracker(HTMLEditorRef().RangeUpdaterRef(),
address_of(startNode), &startOffset);
AutoTrackDOMPoint endTracker(HTMLEditorRef().RangeUpdaterRef(),
address_of(endNode), &endOffset);
// We are handling all ranged deletions directly now.
*aHandled = true;
if (endNode == startNode) {
rv = HTMLEditorRef().DeleteSelectionWithTransaction(aAction,
aStripWrappers);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
// Figure out mailcite ancestors
nsCOMPtr<Element> startCiteNode = GetTopEnclosingMailCite(*startNode);
nsCOMPtr<Element> endCiteNode = GetTopEnclosingMailCite(*endNode);
// If we only have a mailcite at one of the two endpoints, set the
// directionality of the deletion so that the selection will end up
// outside the mailcite.
if (startCiteNode && !endCiteNode) {
aAction = nsIEditor::eNext;
} else if (!startCiteNode && endCiteNode) {
aAction = nsIEditor::ePrevious;
}
// Figure out block parents
nsCOMPtr<Element> leftParent = HTMLEditor::GetBlock(*startNode);
nsCOMPtr<Element> rightParent = HTMLEditor::GetBlock(*endNode);
// Are endpoint block parents the same? Use default deletion
if (leftParent && leftParent == rightParent) {
HTMLEditorRef().DeleteSelectionWithTransaction(aAction, aStripWrappers);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
} else {
// Deleting across blocks. Are the blocks of same type?
if (NS_WARN_IF(!leftParent) || NS_WARN_IF(!rightParent)) {
return NS_ERROR_FAILURE;
}
// Are the blocks siblings?
nsCOMPtr<nsINode> leftBlockParent = leftParent->GetParentNode();
nsCOMPtr<nsINode> rightBlockParent = rightParent->GetParentNode();
// MOOSE: this could conceivably screw up a table.. fix me.
if (leftBlockParent == rightBlockParent &&
HTMLEditorRef().AreNodesSameType(*leftParent, *rightParent) &&
// XXX What's special about these three types of block?
(leftParent->IsHTMLElement(nsGkAtoms::p) ||
HTMLEditUtils::IsListItem(leftParent) ||
HTMLEditUtils::IsHeader(*leftParent))) {
// First delete the selection
rv = HTMLEditorRef().DeleteSelectionWithTransaction(aAction,
aStripWrappers);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Join blocks
EditorDOMPoint pt = HTMLEditorRef().JoinNodesDeepWithTransaction(
*leftParent, *rightParent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!pt.IsSet())) {
return NS_ERROR_FAILURE;
}
// Fix up selection
ErrorResult error;
SelectionRefPtr()->Collapse(pt, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
// Else blocks not same type, or not siblings. Delete everything
// except table elements.
join = true;
AutoRangeArray arrayOfRanges(SelectionRefPtr());
for (auto& range : arrayOfRanges.mRanges) {
// Build a list of nodes in the range
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
TrivialFunctor functor;
DOMSubtreeIterator iter;
nsresult rv = iter.Init(*range);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
iter.AppendList(functor, arrayOfNodes);
// Now that we have the list, delete non-table elements
int32_t listCount = arrayOfNodes.Length();
for (int32_t j = 0; j < listCount; j++) {
OwningNonNull<nsINode> node = arrayOfNodes[0];
nsresult rv = DeleteElementsExceptTableRelatedElements(node);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to elements except table related elements");
arrayOfNodes.RemoveElementAt(0);
// If something visible is deleted, no need to join. Visible means
// all nodes except non-visible textnodes and breaks.
if (join && origCollapsed) {
if (!node->IsContent()) {
join = false;
continue;
}
nsIContent* content = node->AsContent();
if (Text* text = content->GetAsText()) {
join = !HTMLEditorRef().IsInVisibleTextFrames(*text);
} else {
join = content->IsHTMLElement(nsGkAtoms::br) &&
!HTMLEditorRef().IsVisibleBRElement(node);
}
}
}
}
// Check endpoints for possible text deletion. We can assume that if
// text node is found, we can delete to end or to begining as
// appropriate, since the case where both sel endpoints in same text
// node was already handled (we wouldn't be here)
if (startNode->GetAsText() &&
startNode->Length() > static_cast<uint32_t>(startOffset)) {
// Delete to last character
OwningNonNull<CharacterData> dataNode =
*static_cast<CharacterData*>(startNode.get());
rv = HTMLEditorRef().DeleteTextWithTransaction(
dataNode, startOffset, startNode->Length() - startOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (endNode->GetAsText() && endOffset) {
// Delete to first character
OwningNonNull<CharacterData> dataNode =
*static_cast<CharacterData*>(endNode.get());
rv =
HTMLEditorRef().DeleteTextWithTransaction(dataNode, 0, endOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (join) {
EditActionResult ret =
TryToJoinBlocksWithTransaction(*leftParent, *rightParent);
MOZ_ASSERT(*aHandled);
*aCancel |= ret.Canceled();
if (NS_WARN_IF(ret.Failed())) {
return ret.Rv();
}
}
}
}
}
// We might have left only collapsed whitespace in the start/end nodes
{
AutoTrackDOMPoint startTracker(HTMLEditorRef().RangeUpdaterRef(),
address_of(startNode), &startOffset);
AutoTrackDOMPoint endTracker(HTMLEditorRef().RangeUpdaterRef(),
address_of(endNode), &endOffset);
nsresult rv = DeleteNodeIfCollapsedText(*startNode);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to delete start node even though it's collapsed text");
rv = DeleteNodeIfCollapsedText(*endNode);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"Failed to delete end node even though it's collapsed text");
}
// If we're joining blocks: if deleting forward the selection should be
// collapsed to the end of the selection, if deleting backward the selection
// should be collapsed to the beginning of the selection. But if we're not
// joining then the selection should collapse to the beginning of the
// selection if we'redeleting forward, because the end of the selection will
// still be in the next block. And same thing for deleting backwards
// (selection should collapse to the end, because the beginning will still be
// in the first block). See Bug 507936
if (aAction == (join ? nsIEditor::eNext : nsIEditor::ePrevious)) {
rv = SelectionRefPtr()->Collapse(endNode, endOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = SelectionRefPtr()->Collapse(startNode, startOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult HTMLEditRules::DeleteNodeIfCollapsedText(nsINode& aNode) {
MOZ_ASSERT(IsEditorDataAvailable());
Text* text = aNode.GetAsText();
if (!text) {
return NS_OK;
}
if (HTMLEditorRef().IsVisibleTextNode(*text)) {
return NS_OK;
}
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(aNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::InsertBRIfNeeded() {
MOZ_ASSERT(IsEditorDataAvailable());
EditorRawDOMPoint atStartOfSelection(
EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
// inline elements don't need any br
if (!IsBlockNode(*atStartOfSelection.GetContainer())) {
return NS_OK;
}
// examine selection
WSRunObject wsObj(&HTMLEditorRef(), atStartOfSelection);
if (((wsObj.mStartReason & WSType::block) ||
(wsObj.mStartReason & WSType::br)) &&
(wsObj.mEndReason & WSType::block)) {
// if we are tucked between block boundaries then insert a br
// first check that we are allowed to
if (HTMLEditorRef().CanContainTag(*atStartOfSelection.GetContainer(),
*nsGkAtoms::br)) {
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(atStartOfSelection,
nsIEditor::ePrevious);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
return NS_OK;
}
}
return NS_OK;
}
EditorDOMPoint HTMLEditRules::GetGoodSelPointForNode(
nsINode& aNode, nsIEditor::EDirection aAction) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord ||
aAction == nsIEditor::ePrevious ||
aAction == nsIEditor::ePreviousWord ||
aAction == nsIEditor::eToBeginningOfLine ||
aAction == nsIEditor::eToEndOfLine);
bool isPreviousAction =
(aAction == nsIEditor::ePrevious || aAction == nsIEditor::ePreviousWord ||
aAction == nsIEditor::eToBeginningOfLine);
if (aNode.GetAsText() || HTMLEditorRef().IsContainer(&aNode) ||
NS_WARN_IF(!aNode.GetParentNode())) {
return EditorDOMPoint(&aNode, isPreviousAction ? aNode.Length() : 0);
}
if (NS_WARN_IF(!aNode.IsContent())) {
return EditorDOMPoint();
}
EditorDOMPoint ret(&aNode);
if ((!aNode.IsHTMLElement(nsGkAtoms::br) ||
HTMLEditorRef().IsVisibleBRElement(&aNode)) &&
isPreviousAction) {
ret.AdvanceOffset();
}
return ret;
}
EditActionResult HTMLEditRules::TryToJoinBlocksWithTransaction(
nsIContent& aLeftNode, nsIContent& aRightNode) {
MOZ_ASSERT(IsEditorDataAvailable());
RefPtr<Element> leftBlock = HTMLEditorRef().GetBlock(aLeftNode);
RefPtr<Element> rightBlock = HTMLEditorRef().GetBlock(aRightNode);
// Sanity checks
if (NS_WARN_IF(!leftBlock) || NS_WARN_IF(!rightBlock)) {
return EditActionIgnored(NS_ERROR_NULL_POINTER);
}
if (NS_WARN_IF(leftBlock == rightBlock)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
if (HTMLEditUtils::IsTableElement(leftBlock) ||
HTMLEditUtils::IsTableElement(rightBlock)) {
// Do not try to merge table elements
return EditActionCanceled();
}
// Make sure we don't try to move things into HR's, which look like blocks
// but aren't containers
if (leftBlock->IsHTMLElement(nsGkAtoms::hr)) {
leftBlock = HTMLEditorRef().GetBlockNodeParent(leftBlock);
if (NS_WARN_IF(!leftBlock)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
}
if (rightBlock->IsHTMLElement(nsGkAtoms::hr)) {
rightBlock = HTMLEditorRef().GetBlockNodeParent(rightBlock);
if (NS_WARN_IF(!rightBlock)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
}
// Bail if both blocks the same
if (leftBlock == rightBlock) {
return EditActionIgnored();
}
// Joining a list item to its parent is a NOP.
if (HTMLEditUtils::IsList(leftBlock) &&
HTMLEditUtils::IsListItem(rightBlock) &&
rightBlock->GetParentNode() == leftBlock) {
return EditActionHandled();
}
// Special rule here: if we are trying to join list items, and they are in
// different lists, join the lists instead.
bool mergeLists = false;
nsAtom* existingList = nsGkAtoms::_empty;
EditorDOMPoint atChildInBlock;
nsCOMPtr<Element> leftList, rightList;
if (HTMLEditUtils::IsListItem(leftBlock) &&
HTMLEditUtils::IsListItem(rightBlock)) {
leftList = leftBlock->GetParentElement();
rightList = rightBlock->GetParentElement();
if (leftList && rightList && leftList != rightList &&
!EditorUtils::IsDescendantOf(*leftList, *rightBlock, &atChildInBlock) &&
!EditorUtils::IsDescendantOf(*rightList, *leftBlock, &atChildInBlock)) {
// There are some special complications if the lists are descendants of
// the other lists' items. Note that it is okay for them to be
// descendants of the other lists themselves, which is the usual case for
// sublists in our implementation.
MOZ_DIAGNOSTIC_ASSERT(!atChildInBlock.IsSet());
leftBlock = leftList;
rightBlock = rightList;
mergeLists = true;
existingList = leftList->NodeInfo()->NameAtom();
}
}
AutoTransactionsConserveSelection dontChangeMySelection(HTMLEditorRef());
// offset below is where you find yourself in rightBlock when you traverse
// upwards from leftBlock
EditorDOMPoint atRightBlockChild;
if (EditorUtils::IsDescendantOf(*leftBlock, *rightBlock,
&atRightBlockChild)) {
// Tricky case. Left block is inside right block. Do ws adjustment. This
// just destroys non-visible ws at boundaries we will be joining.
DebugOnly<bool> advanced = atRightBlockChild.AdvanceOffset();
NS_WARNING_ASSERTION(
advanced,
"Failed to advance offset to after child of rightBlock, "
"leftBlock is a descendant of the child");
nsresult rv = WSRunObject::ScrubBlockBoundary(
&HTMLEditorRef(), WSRunObject::kBlockEnd, leftBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
{
// We can't just track rightBlock because it's an Element.
AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
&atRightBlockChild);
rv = WSRunObject::ScrubBlockBoundary(
&HTMLEditorRef(), WSRunObject::kAfterBlock,
atRightBlockChild.GetContainer(), atRightBlockChild.Offset());
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
// XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
// Do we really need to do update rightBlock here??
MOZ_ASSERT(rightBlock == atRightBlockChild.GetContainer());
if (atRightBlockChild.GetContainerAsElement()) {
rightBlock = atRightBlockChild.GetContainerAsElement();
} else {
if (NS_WARN_IF(!atRightBlockChild.GetContainer()->GetParentElement())) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
rightBlock = atRightBlockChild.GetContainer()->GetParentElement();
}
}
// Do br adjustment.
RefPtr<Element> brNode =
CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
EditActionResult ret(NS_OK);
if (NS_WARN_IF(mergeLists)) {
// Since 2002, here was the following comment:
// > The idea here is to take all children in rightList that are past
// > offset, and pull them into leftlist.
// However, this has never been performed because we are here only when
// neither left list nor right list is a descendant of the other but
// in such case, getting a list item in the right list node almost
// always failed since a variable for offset of rightList->GetChildAt()
// was not initialized. So, it might be a bug, but we should keep this
// traditional behavior for now. If you find when we get here, please
// remove this comment if we don't need to do it. Otherwise, please
// move children of the right list node to the end of the left list node.
MOZ_DIAGNOSTIC_ASSERT(!atChildInBlock.IsSet());
// XXX Although, we don't do nothing here, but for keeping traditional
// behavior, we should mark as handled.
ret.MarkAsHandled();
} else {
// XXX Why do we ignore the result of MoveBlock()?
EditActionResult retMoveBlock =
MoveBlock(*leftBlock, *rightBlock, -1, atRightBlockChild.Offset());
if (NS_WARN_IF(retMoveBlock.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return ret;
}
NS_WARNING_ASSERTION(
retMoveBlock.Succeeded(),
"Failed to move contents of the right block to the left block");
if (retMoveBlock.Handled()) {
ret.MarkAsHandled();
}
// Now, all children of rightBlock were moved to leftBlock. So,
// atRightBlockChild is now invalid.
atRightBlockChild.Clear();
}
if (brNode) {
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(*brNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_SUCCEEDED(rv)) {
ret.MarkAsHandled();
} else {
NS_WARNING("Failed to remove the <br> element");
}
}
return ret;
}
MOZ_DIAGNOSTIC_ASSERT(!atRightBlockChild.IsSet());
// Offset below is where you find yourself in leftBlock when you traverse
// upwards from rightBlock
EditorDOMPoint leftBlockChild;
if (EditorUtils::IsDescendantOf(*rightBlock, *leftBlock, &leftBlockChild)) {
// Tricky case. Right block is inside left block. Do ws adjustment. This
// just destroys non-visible ws at boundaries we will be joining.
nsresult rv = WSRunObject::ScrubBlockBoundary(
&HTMLEditorRef(), WSRunObject::kBlockStart, rightBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
{
// We can't just track leftBlock because it's an Element, so track
// something else.
AutoTrackDOMPoint tracker(HTMLEditorRef().RangeUpdaterRef(),
&leftBlockChild);
rv = WSRunObject::ScrubBlockBoundary(&HTMLEditorRef(),
WSRunObject::kBeforeBlock, leftBlock,
leftBlockChild.Offset());
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
// XXX AutoTrackDOMPoint instance, tracker, hasn't been destroyed here.
// Do we really need to do update rightBlock here??
MOZ_DIAGNOSTIC_ASSERT(leftBlock == leftBlockChild.GetContainer());
if (leftBlockChild.GetContainerAsElement()) {
leftBlock = leftBlockChild.GetContainerAsElement();
} else {
if (NS_WARN_IF(!leftBlockChild.GetContainer()->GetParentElement())) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
leftBlock = leftBlockChild.GetContainer()->GetParentElement();
}
}
// Do br adjustment.
RefPtr<Element> brNode = CheckForInvisibleBR(
*leftBlock, BRLocation::beforeBlock, leftBlockChild.Offset());
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;
}
NS_WARNING_ASSERTION(
retMoveContents.Succeeded(),
"Failed to move contents from the right list to the left list");
if (retMoveContents.Handled()) {
ret.MarkAsHandled();
}
// leftBlockChild was moved to rightList. So, it's invalid now.
leftBlockChild.Clear();
} else {
// Left block is a parent of right block, and the parent of the previous
// visible content. Right block is a child and contains the contents we
// want to move.
EditorDOMPoint previousContent;
if (&aLeftNode == leftBlock) {
// We are working with valid HTML, aLeftNode is a block node, and is
// therefore allowed to contain rightBlock. This is the simple case,
// we will simply move the content in rightBlock out of its block.
previousContent = leftBlockChild;
} else {
// We try to work as well as possible with HTML that's already invalid.
// Although "right block" is a block, and a block must not be contained
// in inline elements, reality is that broken documents do exist. The
// DIRECT parent of "left NODE" might be an inline element. Previous
// versions of this code skipped inline parents until the first block
// parent was found (and used "left block" as the destination).
// However, in some situations this strategy moves the content to an
// unexpected position. (see bug 200416) The new idea is to make the
// moving content a sibling, next to the previous visible content.
previousContent.Set(&aLeftNode);
// We want to move our content just after the previous visible node.
previousContent.AdvanceOffset();
}
// Because we don't want the moving content to receive the style of the
// previous content, we split the previous content's style.
RefPtr<Element> editorRoot = HTMLEditorRef().GetEditorRoot();
if (!editorRoot || &aLeftNode != editorRoot) {
nsCOMPtr<nsIContent> splittedPreviousContent;
nsCOMPtr<nsINode> previousContentParent =
previousContent.GetContainer();
int32_t previousContentOffset = previousContent.Offset();
rv = HTMLEditorRef().SplitStyleAbovePoint(
address_of(previousContentParent), &previousContentOffset, nullptr,
nullptr, nullptr, getter_AddRefs(splittedPreviousContent));
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
if (splittedPreviousContent) {
previousContent.Set(splittedPreviousContent);
} else {
previousContent.Set(previousContentParent, previousContentOffset);
}
}
if (NS_WARN_IF(!previousContent.IsSet())) {
return EditActionIgnored(NS_ERROR_NULL_POINTER);
}
ret |= MoveBlock(*previousContent.GetContainerAsElement(), *rightBlock,
previousContent.Offset(), 0);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
}
if (brNode) {
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(*brNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_SUCCEEDED(rv)) {
ret.MarkAsHandled();
} else {
NS_WARNING("Failed to remove the <br> element");
}
}
return ret;
}
MOZ_DIAGNOSTIC_ASSERT(!atRightBlockChild.IsSet());
MOZ_DIAGNOSTIC_ASSERT(!leftBlockChild.IsSet());
// Normal case. Blocks are siblings, or at least close enough. An example
// of the latter is <p>paragraph</p><ul><li>one<li>two<li>three</ul>. The
// first li and the p are not true siblings, but we still want to join them
// if you backspace from li into p.
// Adjust whitespace at block boundaries
nsresult rv =
WSRunObject::PrepareToJoinBlocks(&HTMLEditorRef(), leftBlock, rightBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
// Do br adjustment.
nsCOMPtr<Element> brNode =
CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
EditActionResult ret(NS_OK);
if (mergeLists ||
leftBlock->NodeInfo()->NameAtom() == rightBlock->NodeInfo()->NameAtom()) {
// Nodes are same type. merge them.
EditorDOMPoint pt;
nsresult rv =
JoinNearestEditableNodesWithTransaction(*leftBlock, *rightBlock, &pt);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
if (pt.IsSet() && mergeLists) {
CreateElementResult convertListTypeResult =
ConvertListType(*rightBlock, *existingList, *nsGkAtoms::li);
if (NS_WARN_IF(convertListTypeResult.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return EditActionIgnored(NS_ERROR_EDITOR_DESTROYED);
}
}
ret.MarkAsHandled();
} else {
// Nodes are dissimilar types.
ret |= MoveBlock(*leftBlock, *rightBlock, -1, 0);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
}
if (brNode) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*brNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
// XXX In other top level if blocks, the result of
// DeleteNodeWithTransaction() is ignored. Why does only this result
// is respected?
if (NS_WARN_IF(NS_FAILED(rv))) {
return ret.SetResult(rv);
}
ret.MarkAsHandled();
}
return ret;
}
EditActionResult HTMLEditRules::MoveBlock(Element& aLeftBlock,
Element& aRightBlock,
int32_t aLeftOffset,
int32_t aRightOffset) {
MOZ_ASSERT(IsEditorDataAvailable());
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
// GetNodesFromPoint is the workhorse that figures out what we wnat to move.
nsresult rv = GetNodesFromPoint(EditorDOMPoint(&aRightBlock, aRightOffset),
EditSubAction::eCreateOrChangeList,
arrayOfNodes, TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
EditActionResult ret(NS_OK);
for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
// get the node to act on
if (IsBlockNode(arrayOfNodes[i])) {
// For block nodes, move their contents only, then delete block.
ret |=
MoveContents(*arrayOfNodes[i]->AsElement(), aLeftBlock, &aLeftOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
rv = HTMLEditorRef().DeleteNodeWithTransaction(*arrayOfNodes[i]);
if (NS_WARN_IF(!CanHandleEditAction())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove a block node");
ret.MarkAsHandled();
} else {
// Otherwise move the content as is, checking against the DTD.
ret |= MoveNodeSmart(*arrayOfNodes[i]->AsContent(), aLeftBlock,
&aLeftOffset);
if (NS_WARN_IF(ret.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return ret;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to move current node to the left block");
}
}
// XXX We're only checking return value of the last iteration
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
return ret;
}
EditActionResult HTMLEditRules::MoveNodeSmart(nsIContent& aNode,
Element& aDestElement,
int32_t* aInOutDestOffset) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aInOutDestOffset);
// Check if this node can go into the destination node
if (HTMLEditorRef().CanContain(aDestElement, aNode)) {
// If it can, move it there.
if (*aInOutDestOffset == -1) {
nsresult rv =
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 {
EditorRawDOMPoint pointToInsert(&aDestElement, *aInOutDestOffset);
nsresult rv =
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);
}
}
if (*aInOutDestOffset != -1) {
(*aInOutDestOffset)++;
}
// XXX Should we check if the node is actually moved in this case?
return EditActionHandled();
}
// If it can't, move its children (if any), and then delete it.
EditActionResult ret(NS_OK);
if (aNode.IsElement()) {
ret = MoveContents(*aNode.AsElement(), aDestElement, aInOutDestOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
}
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(aNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return ret.SetResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return ret.SetResult(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(*aElement.GetFirstChild(), aDestElement,
aInOutDestOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
}
return ret;
}
nsresult HTMLEditRules::DeleteElementsExceptTableRelatedElements(
nsINode& aNode) {
MOZ_ASSERT(IsEditorDataAvailable());
if (!HTMLEditUtils::IsTableElementButNotTable(&aNode)) {
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(aNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// XXX For performance, this should just call
// DeleteElementsExceptTableRelatedElements() while there are children
// in aNode. If we need to avoid infinite loop because mutation event
// listeners can add unexpected nodes into aNode, we should just loop
// only original count of the children.
AutoTArray<OwningNonNull<nsIContent>, 10> childList;
for (nsIContent* child = aNode.GetFirstChild(); child;
child = child->GetNextSibling()) {
childList.AppendElement(*child);
}
for (const auto& child : childList) {
nsresult rv = DeleteElementsExceptTableRelatedElements(child);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult HTMLEditRules::DidDeleteSelection() {
MOZ_ASSERT(IsEditorDataAvailable());
// find where we are
EditorDOMPoint atStartOfSelection(
EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
// find any enclosing mailcite
RefPtr<Element> citeNode =
GetTopEnclosingMailCite(*atStartOfSelection.GetContainer());
if (citeNode) {
bool isEmpty = true, seenBR = false;
HTMLEditorRef().IsEmptyNodeImpl(citeNode, &isEmpty, true, true, false,
&seenBR);
if (isEmpty) {
EditorDOMPoint atCiteNode(citeNode);
{
AutoEditorDOMPointChildInvalidator lockOffset(atCiteNode);
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(*citeNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (atCiteNode.IsSet() && seenBR) {
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(atCiteNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
IgnoredErrorResult error;
SelectionRefPtr()->Collapse(EditorRawDOMPoint(brElement), error);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!error.Failed(),
"Failed to collapse selection at the new <br> element");
}
}
}
// call through to base class
nsresult rv = TextEditRules::DidDeleteSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::WillMakeList(const nsAString* aListType,
bool aEntireList,
const nsAString* aBulletType,
bool* aCancel, bool* aHandled,
const nsAString* aItemType) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aListType) || NS_WARN_IF(!aCancel) || NS_WARN_IF(!aHandled)) {
return NS_ERROR_INVALID_ARG;
}
*aCancel = false;
*aHandled = false;
OwningNonNull<nsAtom> listType = NS_Atomize(*aListType);
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
// deduce what tag to use for list items
RefPtr<nsAtom> itemType;
if (aItemType) {
itemType = NS_Atomize(*aItemType);
} else if (listType == nsGkAtoms::dl) {
itemType = nsGkAtoms::dd;
} else {
itemType = nsGkAtoms::li;
}
// convert the selection ranges into "promoted" selection ranges:
// this basically just expands the range to include the immediate
// block parent, and then further expands to include any ancestors
// whose children are all in the range
*aHandled = true;
rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// MakeList() creates AutoSelectionRestorer.
// Therefore, even if it returns NS_OK, editor might have been destroyed
// at restoring Selection.
rv = MakeList(listType, aEntireList, aBulletType, aCancel, *itemType);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) ||
NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::MakeList(nsAtom& aListType, bool aEntireList,
const nsAString* aBulletType, bool* aCancel,
nsAtom& aItemType) {
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetListActionNodes(
arrayOfNodes, aEntireList ? EntireList::yes : EntireList::no,
TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// check if all our nodes are <br>s, or empty inlines
bool bOnlyBreaks = true;
for (auto& curNode : arrayOfNodes) {
// if curNode is not a Break or empty inline, we're done
if (!TextEditUtils::IsBreak(curNode) && !IsEmptyInline(curNode)) {
bOnlyBreaks = false;
break;
}
}
// if no nodes, we make empty list. Ditto if the user tried to make a list
// of some # of breaks.
if (arrayOfNodes.IsEmpty() || bOnlyBreaks) {
// if only breaks, delete them
if (bOnlyBreaks) {
for (auto& node : arrayOfNodes) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*node);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
// Make sure we can put a list here.
if (!HTMLEditorRef().CanContainTag(*atStartOfSelection.GetContainer(),
aListType)) {
*aCancel = true;
return NS_OK;
}
SplitNodeResult splitAtSelectionStartResult =
MaybeSplitAncestorsForInsertWithTransaction(aListType,
atStartOfSelection);
if (NS_WARN_IF(splitAtSelectionStartResult.Failed())) {
return splitAtSelectionStartResult.Rv();
}
RefPtr<Element> theList = HTMLEditorRef().CreateNodeWithTransaction(
aListType, splitAtSelectionStartResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!theList)) {
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint atFirstListItemToInsertBefore(theList, 0);
RefPtr<Element> theListItem = HTMLEditorRef().CreateNodeWithTransaction(
aItemType, atFirstListItemToInsertBefore);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!theListItem)) {
return NS_ERROR_FAILURE;
}
// remember our new block for postprocessing
mNewBlock = theListItem;
// Put selection in new list item and don't restore the Selection.
restoreSelectionLater.Abort();
ErrorResult error;
SelectionRefPtr()->Collapse(EditorRawDOMPoint(theListItem, 0), error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
// if there is only one node in the array, and it is a list, div, or
// blockquote, then look inside of it until we find inner list or content.
LookInsideDivBQandList(arrayOfNodes);
// Ok, now go through all the nodes and put then in the list,
// or whatever is approriate. Wohoo!
uint32_t listCount = arrayOfNodes.Length();
RefPtr<Element> curList, prevListItem;
for (uint32_t i = 0; i < listCount; i++) {
// here's where we actually figure out what to do
RefPtr<Element> newBlock;
if (NS_WARN_IF(!arrayOfNodes[i]->IsContent())) {
return NS_ERROR_FAILURE;
}
OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
// make sure we don't assemble content that is in different table cells
// into the same list. respect table cell boundaries when listifying.
if (curList && InDifferentTableElements(curList, curNode)) {
curList = nullptr;
}
// If curNode is a break, delete it, and quit remembering prev list item.
// If an empty inline container, delete it, but still remember the previous
// item.
if (HTMLEditorRef().IsEditable(curNode) &&
(TextEditUtils::IsBreak(curNode) || IsEmptyInline(curNode))) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*curNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (TextEditUtils::IsBreak(curNode)) {
prevListItem = nullptr;
}
continue;
}
if (HTMLEditUtils::IsList(curNode)) {
// do we have a curList already?
if (curList && !EditorUtils::IsDescendantOf(*curNode, *curList)) {
// move all of our children into curList. cheezy way to do it: move
// whole list and then RemoveContainerWithTransaction() on the list.
// ConvertListType first: that routine handles converting the list
// item types, if needed.
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode, *curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
CreateElementResult convertListTypeResult =
ConvertListType(*curNode->AsElement(), aListType, aItemType);
if (NS_WARN_IF(convertListTypeResult.Failed())) {
return convertListTypeResult.Rv();
}
rv = HTMLEditorRef().RemoveBlockContainerWithTransaction(
*convertListTypeResult.GetNewNode());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
newBlock = convertListTypeResult.forget();
} else {
// replace list with new list type
CreateElementResult convertListTypeResult =
ConvertListType(*curNode->AsElement(), aListType, aItemType);
if (NS_WARN_IF(convertListTypeResult.Failed())) {
return convertListTypeResult.Rv();
}
curList = convertListTypeResult.forget();
}
prevListItem = nullptr;
continue;
}
EditorRawDOMPoint atCurNode(curNode);
if (NS_WARN_IF(!atCurNode.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(atCurNode.IsSetAndValid());
if (HTMLEditUtils::IsListItem(curNode)) {
if (!atCurNode.IsContainerHTMLElement(&aListType)) {
// list item is in wrong type of list. if we don't have a curList,
// split the old list and make a new list of correct type.
if (!curList || EditorUtils::IsDescendantOf(*curNode, *curList)) {
if (NS_WARN_IF(!atCurNode.GetContainerAsContent())) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
nsCOMPtr<nsIContent> newLeftNode =
HTMLEditorRef().SplitNodeWithTransaction(atCurNode, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
newBlock = newLeftNode ? newLeftNode->AsElement() : nullptr;
EditorRawDOMPoint atParentOfCurNode(atCurNode.GetContainer());
curList = HTMLEditorRef().CreateNodeWithTransaction(
aListType, atParentOfCurNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curList)) {
return NS_ERROR_FAILURE;
}
}
// move list item to new list
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode, *curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// convert list item type if needed
if (!curNode->IsHTMLElement(&aItemType)) {
newBlock = HTMLEditorRef().ReplaceContainerWithTransaction(
*curNode->AsElement(), aItemType);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!newBlock)) {
return NS_ERROR_FAILURE;
}
}
} else {
// item is in right type of list. But we might still have to move it.
// and we might need to convert list item types.
if (!curList) {
curList = atCurNode.GetContainerAsElement();
} else if (atCurNode.GetContainer() != curList) {
// move list item to new list
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode, *curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (!curNode->IsHTMLElement(&aItemType)) {
newBlock = HTMLEditorRef().ReplaceContainerWithTransaction(
*curNode->AsElement(), aItemType);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!newBlock)) {
return NS_ERROR_FAILURE;
}
}
}
nsCOMPtr<Element> curElement = do_QueryInterface(curNode);
if (NS_WARN_IF(!curElement)) {
return NS_ERROR_FAILURE;
}
if (aBulletType && !aBulletType->IsEmpty()) {
rv = HTMLEditorRef().SetAttributeWithTransaction(
*curElement, *nsGkAtoms::type, *aBulletType);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = HTMLEditorRef().RemoveAttributeWithTransaction(*curElement,
*nsGkAtoms::type);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
continue;
}
// if we hit a div clear our prevListItem, insert divs contents
// into our node array, and remove the div
if (curNode->IsHTMLElement(nsGkAtoms::div)) {
prevListItem = nullptr;
int32_t j = i + 1;
GetInnerContent(*curNode, arrayOfNodes, &j);
rv =
HTMLEditorRef().RemoveContainerWithTransaction(*curNode->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
listCount = arrayOfNodes.Length();
continue;
}
// need to make a list to put things in if we haven't already,
if (!curList) {
SplitNodeResult splitCurNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(aListType, atCurNode);
if (NS_WARN_IF(splitCurNodeResult.Failed())) {
return splitCurNodeResult.Rv();
}
curList = HTMLEditorRef().CreateNodeWithTransaction(
aListType, splitCurNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curList)) {
return NS_ERROR_FAILURE;
}
// remember our new block for postprocessing
mNewBlock = curList;
// curList is now the correct thing to put curNode in
prevListItem = nullptr;
// atCurNode is now referring the right node with mOffset but
// referring the left node with mRef. So, invalidate it now.
atCurNode.Clear();
}
// if curNode isn't a list item, we must wrap it in one
nsCOMPtr<Element> listItem;
if (!HTMLEditUtils::IsListItem(curNode)) {
if (IsInlineNode(curNode) && prevListItem) {
// this is a continuation of some inline nodes that belong together in
// the same list item. use prevListItem
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode,
*prevListItem);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
// don't wrap li around a paragraph. instead replace paragraph with li
if (curNode->IsHTMLElement(nsGkAtoms::p)) {
listItem = HTMLEditorRef().ReplaceContainerWithTransaction(
*curNode->AsElement(), aItemType);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!listItem)) {
return NS_ERROR_FAILURE;
}
} else {
listItem = HTMLEditorRef().InsertContainerWithTransaction(*curNode,
aItemType);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!listItem)) {
return NS_ERROR_FAILURE;
}
}
if (IsInlineNode(curNode)) {
prevListItem = listItem;
} else {
prevListItem = nullptr;
}
}
} else {
listItem = curNode->AsElement();
}
if (listItem) {
// if we made a new list item, deal with it: tuck the listItem into the
// end of the active list
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*listItem, *curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
return NS_OK;
}
nsresult HTMLEditRules::WillRemoveList(bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aCancel) || NS_WARN_IF(!aHandled)) {
return NS_ERROR_INVALID_ARG;
}
// initialize out param
*aCancel = false;
*aHandled = true;
nsresult rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(arrayOfRanges, EditSubAction::eCreateOrChangeList);
// use these ranges to contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetListActionNodes(arrayOfNodes, EntireList::no, TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Remove all non-editable nodes. Leave them be.
for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> testNode = arrayOfNodes[i];
if (!HTMLEditorRef().IsEditable(testNode)) {
arrayOfNodes.RemoveElementAt(i);
}
}
// Only act on lists or list items in the array
for (auto& curNode : arrayOfNodes) {
// here's where we actually figure out what to do
if (HTMLEditUtils::IsListItem(curNode)) {
// unlist this listitem
bool bOutOfList;
do {
rv = PopListItem(*curNode->AsContent(), &bOutOfList);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} while (
!bOutOfList); // keep popping it out until it's not in a list anymore
} else if (HTMLEditUtils::IsList(curNode)) {
// node is a list, move list items out
rv = RemoveListStructure(*curNode->AsElement());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
return NS_OK;
}
nsresult HTMLEditRules::WillMakeDefListItem(const nsAString* aItemType,
bool aEntireList, bool* aCancel,
bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
// for now we let WillMakeList handle this
NS_NAMED_LITERAL_STRING(listType, "dl");
nsresult rv = WillMakeList(&listType.AsString(), aEntireList, nullptr,
aCancel, aHandled, aItemType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::WillMakeBasicBlock(const nsAString& aBlockType,
bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aCancel && aHandled);
OwningNonNull<nsAtom> blockType = NS_Atomize(aBlockType);
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
*aCancel = false;
*aHandled = true;
// MakeBasicBlock() creates AutoSelectionRestorer.
// Therefore, even if it returns NS_OK, editor might have been destroyed
// at restoring Selection.
rv = MakeBasicBlock(blockType);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::MakeBasicBlock(nsAtom& blockType) {
MOZ_ASSERT(IsEditorDataAvailable());
nsresult rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
AutoTransactionsConserveSelection dontChangeMySelection(HTMLEditorRef());
// Contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetNodesFromSelection(EditSubAction::eCreateOrRemoveBlock, arrayOfNodes,
TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// If nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint pointToInsertBlock(firstRange->StartRef());
if (&blockType == nsGkAtoms::normal || &blockType == nsGkAtoms::_empty) {
// We are removing blocks (going to "body text")
RefPtr<Element> curBlock =
HTMLEditorRef().GetBlock(*pointToInsertBlock.GetContainer());
if (NS_WARN_IF(!curBlock)) {
return NS_ERROR_FAILURE;
}
if (!HTMLEditUtils::IsFormatNode(curBlock)) {
return NS_OK;
}
// If the first editable node after selection is a br, consume it.
// Otherwise it gets pushed into a following block after the split,
// which is visually bad.
nsCOMPtr<nsIContent> brContent =
HTMLEditorRef().GetNextEditableHTMLNode(pointToInsertBlock);
if (brContent && brContent->IsHTMLElement(nsGkAtoms::br)) {
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
rv = HTMLEditorRef().DeleteNodeWithTransaction(*brContent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Do the splits!
SplitNodeResult splitNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*curBlock, pointToInsertBlock,
SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
EditorRawDOMPoint pointToInsertBrNode(splitNodeResult.SplitPoint());
// Put a <br> element at the split point
brContent =
HTMLEditorRef().InsertBrElementWithTransaction(pointToInsertBrNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brContent)) {
return NS_ERROR_FAILURE;
}
// Put selection at the split point
EditorRawDOMPoint atBrNode(brContent);
// Don't restore the selection
restoreSelectionLater.Abort();
ErrorResult error;
SelectionRefPtr()->Collapse(atBrNode, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
// We are making a block. Consume a br, if needed.
nsCOMPtr<nsIContent> brNode =
HTMLEditorRef().GetNextEditableHTMLNodeInBlock(pointToInsertBlock);
if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertBlock);
rv = HTMLEditorRef().DeleteNodeWithTransaction(*brNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We don't need to act on this node any more
arrayOfNodes.RemoveElement(brNode);
}
// Make sure we can put a block here.
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(blockType,
pointToInsertBlock);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
RefPtr<Element> block = HTMLEditorRef().CreateNodeWithTransaction(
blockType, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!block)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
mNewBlock = block;
// Delete anything that was in the list of nodes
while (!arrayOfNodes.IsEmpty()) {
OwningNonNull<nsINode> curNode = arrayOfNodes[0];
rv = HTMLEditorRef().DeleteNodeWithTransaction(*curNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
arrayOfNodes.RemoveElementAt(0);
}
// Don't restore the selection
restoreSelectionLater.Abort();
// Put selection in new block
rv = SelectionRefPtr()->Collapse(block, 0);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// Okay, now go through all the nodes and make the right kind of blocks, or
// whatever is approriate. Woohoo! Note: blockquote is handled a little
// differently.
if (&blockType == nsGkAtoms::blockquote) {
rv = MakeBlockquote(arrayOfNodes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else if (&blockType == nsGkAtoms::normal ||
&blockType == nsGkAtoms::_empty) {
rv = RemoveBlockStyle(arrayOfNodes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = ApplyBlockStyle(arrayOfNodes, blockType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult HTMLEditRules::DidMakeBasicBlock() {
MOZ_ASSERT(IsEditorDataAvailable());
// check for empty block. if so, put a moz br in it.
if (!SelectionRefPtr()->IsCollapsed()) {
return NS_OK;
}
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
const RangeBoundary& atStartOfSelection = firstRange->StartRef();
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
nsresult rv = InsertMozBRIfNeeded(*atStartOfSelection.Container());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::WillIndent(bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
if (HTMLEditorRef().IsCSSEnabled()) {
nsresult rv = WillCSSIndent(aCancel, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
nsresult rv = WillHTMLIndent(aCancel, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult HTMLEditRules::WillCSSIndent(bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aCancel) || NS_WARN_IF(!aHandled)) {
return NS_ERROR_INVALID_ARG;
}
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
*aCancel = false;
*aHandled = true;
rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// IndentAroundSelectionWithCSS() creates AutoSelectionRestorer.
// Therefore, even if it returns NS_OK, editor might have been destroyed
// at restoring Selection.
rv = IndentAroundSelectionWithCSS();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::IndentAroundSelectionWithCSS() {
MOZ_ASSERT(IsEditorDataAvailable());
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
nsTArray<OwningNonNull<nsRange>> arrayOfRanges;
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
// short circuit: detect case of collapsed selection inside an <li>.
// just sublist that <li>. This prevents bug 97797.
nsCOMPtr<Element> liNode;
if (SelectionRefPtr()->IsCollapsed()) {
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(*SelectionRefPtr()));
if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
Element* block =
HTMLEditorRef().GetBlock(*selectionStartPoint.GetContainer());
if (block && HTMLEditUtils::IsListItem(block)) {
liNode = block;
}
}
if (liNode) {
arrayOfNodes.AppendElement(*liNode);
} else {
// convert the selection ranges into "promoted" selection ranges:
// this basically just expands the range to include the immediate
// block parent, and then further expands to include any ancestors
// whose children are all in the range
nsresult rv = GetNodesFromSelection(EditSubAction::eIndent, arrayOfNodes,
TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// if nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
// get selection location
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
// make sure we can put a block here
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div,
atStartOfSelection);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
RefPtr<Element> theBlock = HTMLEditorRef().CreateNodeWithTransaction(
*nsGkAtoms::div, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!theBlock)) {
return NS_ERROR_FAILURE;
}
// remember our new block for postprocessing
mNewBlock = theBlock;
nsresult rv = IncreaseMarginToIndent(*theBlock);
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to increase indentation");
// delete anything that was in the list of nodes
while (!arrayOfNodes.IsEmpty()) {
OwningNonNull<nsINode> curNode = arrayOfNodes[0];
rv = HTMLEditorRef().DeleteNodeWithTransaction(*curNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
arrayOfNodes.RemoveElementAt(0);
}
// put selection in new block
EditorRawDOMPoint atStartOfTheBlock(theBlock, 0);
// Don't restore the selection
restoreSelectionLater.Abort();
ErrorResult error;
SelectionRefPtr()->Collapse(atStartOfTheBlock, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
// Ok, now go through all the nodes and put them in a blockquote,
// or whatever is appropriate. Wohoo!
nsCOMPtr<Element> curList, curQuote;
nsCOMPtr<nsIContent> sibling;
for (OwningNonNull<nsINode>& curNode : arrayOfNodes) {
// Here's where we actually figure out what to do.
EditorDOMPoint atCurNode(curNode);
if (NS_WARN_IF(!atCurNode.IsSet())) {
continue;
}
// Ignore all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(curNode)) {
continue;
}
// some logic for putting list items into nested lists...
if (HTMLEditUtils::IsList(atCurNode.GetContainer())) {
// Check for whether we should join a list that follows curNode.
// We do this if the next element is a list, and the list is of the
// same type (li/ol) as curNode was a part it.
sibling = HTMLEditorRef().GetNextHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
atCurNode.GetContainer()->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
atCurNode.GetContainer()->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
nsresult rv = HTMLEditorRef().MoveNodeWithTransaction(
*curNode->AsContent(), EditorRawDOMPoint(sibling, 0));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// Check for whether we should join a list that preceeds curNode.
// We do this if the previous element is a list, and the list is of
// the same type (li/ol) as curNode was a part of.
sibling = HTMLEditorRef().GetPriorHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
atCurNode.GetContainer()->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
atCurNode.GetContainer()->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
nsresult rv = HTMLEditorRef().MoveNodeToEndWithTransaction(
*curNode->AsContent(), *sibling);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// check to see if curList is still appropriate. Which it is if
// curNode is still right after it in the same list.
sibling = nullptr;
if (curList) {
sibling = HTMLEditorRef().GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
nsAtom* containerName =
atCurNode.GetContainer()->NodeInfo()->NameAtom();
// Create a new nested list of correct type.
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*containerName,
atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curList = HTMLEditorRef().CreateNodeWithTransaction(
*containerName, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curList)) {
return NS_ERROR_FAILURE;
}
// curList is now the correct thing to put curNode in
// remember our new block for postprocessing
mNewBlock = curList;
}
// tuck the node into the end of the active list
nsresult rv = HTMLEditorRef().MoveNodeToEndWithTransaction(
*curNode->AsContent(), *curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// Not a list item.
if (IsBlockNode(*curNode)) {
nsresult rv = IncreaseMarginToIndent(*curNode->AsElement());
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to inrease indentation");
curQuote = nullptr;
continue;
}
if (!curQuote) {
// First, check that our element can contain a div.
if (!HTMLEditorRef().CanContainTag(*atCurNode.GetContainer(),
*nsGkAtoms::div)) {
return NS_OK; // cancelled
}
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div,
atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curQuote = HTMLEditorRef().CreateNodeWithTransaction(
*nsGkAtoms::div, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curQuote)) {
return NS_ERROR_FAILURE;
}
nsresult rv = IncreaseMarginToIndent(*curQuote);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to increase indentation");
// remember our new block for postprocessing
mNewBlock = curQuote;
// curQuote is now the correct thing to put curNode in
}
// tuck the node into the end of the active blockquote
nsresult rv = HTMLEditorRef().MoveNodeToEndWithTransaction(
*curNode->AsContent(), *curQuote);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult HTMLEditRules::WillHTMLIndent(bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aCancel) || NS_WARN_IF(!aHandled)) {
return NS_ERROR_INVALID_ARG;
}
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
*aCancel = false;
*aHandled = true;
rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// IndentAroundSelectionWithHTML() creates AutoSelectionRestorer.
// Therefore, even if it returns NS_OK, editor might have been destroyed
// at restoring Selection.
rv = IndentAroundSelectionWithHTML();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::IndentAroundSelectionWithHTML() {
MOZ_ASSERT(IsEditorDataAvailable());
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
// convert the selection ranges into "promoted" selection ranges:
// this basically just expands the range to include the immediate
// block parent, and then further expands to include any ancestors
// whose children are all in the range
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(arrayOfRanges, EditSubAction::eIndent);
// use these ranges to contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
EditSubAction::eIndent, TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// if nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
// Make sure we can put a block here.
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::blockquote,
atStartOfSelection);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
RefPtr<Element> theBlock = HTMLEditorRef().CreateNodeWithTransaction(
*nsGkAtoms::blockquote, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!theBlock)) {
return NS_ERROR_FAILURE;
}
// remember our new block for postprocessing
mNewBlock = theBlock;
// delete anything that was in the list of nodes
while (!arrayOfNodes.IsEmpty()) {
OwningNonNull<nsINode> curNode = arrayOfNodes[0];
rv = HTMLEditorRef().DeleteNodeWithTransaction(*curNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
arrayOfNodes.RemoveElementAt(0);
}
EditorRawDOMPoint atStartOfTheBlock(theBlock, 0);
// Don't restore the selection
restoreSelectionLater.Abort();
ErrorResult error;
SelectionRefPtr()->Collapse(atStartOfTheBlock, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
// Ok, now go through all the nodes and put them in a blockquote,
// or whatever is appropriate. Wohoo!
nsCOMPtr<nsIContent> sibling;
nsCOMPtr<Element> curList, curQuote, indentedLI;
for (OwningNonNull<nsINode>& curNode : arrayOfNodes) {
// Here's where we actually figure out what to do.
EditorDOMPoint atCurNode(curNode);
if (NS_WARN_IF(!atCurNode.IsSet())) {
continue;
}
// Ignore all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(curNode)) {
continue;
}
// some logic for putting list items into nested lists...
if (HTMLEditUtils::IsList(atCurNode.GetContainer())) {
// Check for whether we should join a list that follows curNode.
// We do this if the next element is a list, and the list is of the
// same type (li/ol) as curNode was a part it.
sibling = HTMLEditorRef().GetNextHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
atCurNode.GetContainer()->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
atCurNode.GetContainer()->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
rv = HTMLEditorRef().MoveNodeWithTransaction(
*curNode->AsContent(), EditorRawDOMPoint(sibling, 0));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// Check for whether we should join a list that preceeds curNode.
// We do this if the previous element is a list, and the list is of
// the same type (li/ol) as curNode was a part of.
sibling = HTMLEditorRef().GetPriorHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
atCurNode.GetContainer()->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
atCurNode.GetContainer()->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*sibling);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// check to see if curList is still appropriate. Which it is if
// curNode is still right after it in the same list.
sibling = nullptr;
if (curList) {
sibling = HTMLEditorRef().GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
nsAtom* containerName =
atCurNode.GetContainer()->NodeInfo()->NameAtom();
// Create a new nested list of correct type.
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*containerName,
atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curList = HTMLEditorRef().CreateNodeWithTransaction(
*containerName, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curList)) {
return NS_ERROR_FAILURE;
}
// curList is now the correct thing to put curNode in
// remember our new block for postprocessing
mNewBlock = curList;
}
// tuck the node into the end of the active list
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// forget curQuote, if any
curQuote = nullptr;
continue;
}
// Not a list item, use blockquote?
// if we are inside a list item, we don't want to blockquote, we want
// to sublist the list item. We may have several nodes listed in the
// array of nodes to act on, that are in the same list item. Since
// we only want to indent that li once, we must keep track of the most
// recent indented list item, and not indent it if we find another node
// to act on that is still inside the same li.
RefPtr<Element> listItem = IsInListItem(curNode);
if (listItem) {
if (indentedLI == listItem) {
// already indented this list item
continue;
}
// check to see if curList is still appropriate. Which it is if
// curNode is still right after it in the same list.
if (curList) {
sibling = HTMLEditorRef().GetPriorHTMLSibling(listItem);
}
if (!curList || (sibling && sibling != curList)) {
EditorDOMPoint atListItem(listItem);
if (NS_WARN_IF(!listItem)) {
return NS_ERROR_FAILURE;
}
nsAtom* containerName =
atListItem.GetContainer()->NodeInfo()->NameAtom();
// Create a new nested list of correct type.
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*containerName,
atListItem);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curList = HTMLEditorRef().CreateNodeWithTransaction(
*containerName, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curList)) {
return NS_ERROR_FAILURE;
}
}
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*listItem, *curList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// remember we indented this li
indentedLI = listItem;
continue;
}
// need to make a blockquote to put things in if we haven't already,
// or if this node doesn't go in blockquote we used earlier.
// One reason it might not go in prio blockquote is if we are now
// in a different table cell.
if (curQuote && InDifferentTableElements(curQuote, curNode)) {
curQuote = nullptr;
}
if (!curQuote) {
// First, check that our element can contain a blockquote.
if (!HTMLEditorRef().CanContainTag(*atCurNode.GetContainer(),
*nsGkAtoms::blockquote)) {
return NS_OK; // cancelled
}
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::blockquote,
atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curQuote = HTMLEditorRef().CreateNodeWithTransaction(
*nsGkAtoms::blockquote, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curQuote)) {
return NS_ERROR_FAILURE;
}
// remember our new block for postprocessing
mNewBlock = curQuote;
// curQuote is now the correct thing to put curNode in
}
// tuck the node into the end of the active blockquote
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curQuote);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// forget curList, if any
curList = nullptr;
}
return NS_OK;
}
nsresult HTMLEditRules::WillOutdent(bool* aCancel, bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aCancel && aHandled);
*aCancel = false;
*aHandled = true;
nsresult rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// OutdentAroundSelection() creates AutoSelectionRestorer. Therefore,
// even if it returns NS_OK, the editor might have been destroyed at
// restoring Selection.
SplitRangeOffFromNodeResult outdentResult = OutdentAroundSelection();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(outdentResult.Failed())) {
return outdentResult.Rv();
}
// Make sure selection didn't stick to last piece of content in old bq (only
// a problem for collapsed selections)
if (!outdentResult.GetLeftContent() && !outdentResult.GetRightContent()) {
return NS_OK;
}
if (!SelectionRefPtr()->IsCollapsed()) {
return NS_OK;
}
// Push selection past end of left element of last split indented element.
if (outdentResult.GetLeftContent()) {
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_OK;
}
const RangeBoundary& atStartOfSelection = firstRange->StartRef();
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
if (atStartOfSelection.Container() == outdentResult.GetLeftContent() ||
EditorUtils::IsDescendantOf(*atStartOfSelection.Container(),
*outdentResult.GetLeftContent())) {
// Selection is inside the left node - push it past it.
EditorRawDOMPoint afterRememberedLeftBQ(outdentResult.GetLeftContent());
afterRememberedLeftBQ.AdvanceOffset();
IgnoredErrorResult error;
SelectionRefPtr()->Collapse(afterRememberedLeftBQ, error);
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!error.Failed(),
"Failed to collapse selection after the left <blockquote>");
}
}
// And pull selection before beginning of right element of last split
// indented element.
if (outdentResult.GetRightContent()) {
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
const RangeBoundary& atStartOfSelection = firstRange->StartRef();
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
if (atStartOfSelection.Container() == outdentResult.GetRightContent() ||
EditorUtils::IsDescendantOf(*atStartOfSelection.Container(),
*outdentResult.GetRightContent())) {
// Selection is inside the right element - push it before it.
EditorRawDOMPoint atRememberedRightBQ(outdentResult.GetRightContent());
IgnoredErrorResult error;
SelectionRefPtr()->Collapse(atRememberedRightBQ, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(
!error.Failed(),
"Failed to collapse selection after the right <blockquote>");
}
}
return NS_OK;
}
SplitRangeOffFromNodeResult HTMLEditRules::OutdentAroundSelection() {
MOZ_ASSERT(IsEditorDataAvailable());
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
bool useCSS = HTMLEditorRef().IsCSSEnabled();
// Convert the selection ranges into "promoted" selection ranges: this
// basically just expands the range to include the immediate block parent,
// and then further expands to include any ancestors whose children are all
// in the range
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetNodesFromSelection(EditSubAction::eOutdent, arrayOfNodes,
TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
// Okay, now go through all the nodes and remove a level of blockquoting,
// or whatever is appropriate. Wohoo!
nsCOMPtr<nsIContent> leftContentOfLastOutdented;
nsCOMPtr<nsIContent> middleContentOfLastOutdented;
nsCOMPtr<nsIContent> rightContentOfLastOutdented;
nsCOMPtr<Element> curBlockQuote;
nsCOMPtr<nsIContent> firstBQChild, lastBQChild;
bool curBlockQuoteIsIndentedWithCSS = false;
for (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
if (!arrayOfNodes[i]->IsContent()) {
continue;
}
OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
// Here's where we actually figure out what to do
int32_t offset;
nsCOMPtr<nsINode> curParent = EditorBase::GetNodeLocation(curNode, &offset);
if (!curParent) {
continue;
}
// Is it a blockquote?
if (curNode->IsHTMLElement(nsGkAtoms::blockquote)) {
// If it is a blockquote, remove it. So we need to finish up dealng
// with any curBlockQuote first.
if (curBlockQuote) {
SplitRangeOffFromNodeResult outdentResult =
OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS);
if (NS_WARN_IF(outdentResult.Failed())) {
return outdentResult;
}
leftContentOfLastOutdented = outdentResult.GetLeftContent();
middleContentOfLastOutdented = outdentResult.GetMiddleContent();
rightContentOfLastOutdented = outdentResult.GetRightContent();
curBlockQuote = nullptr;
firstBQChild = nullptr;
lastBQChild = nullptr;
curBlockQuoteIsIndentedWithCSS = false;
}
rv = HTMLEditorRef().RemoveBlockContainerWithTransaction(
*curNode->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
continue;
}
// Is it a block with a 'margin' property?
if (useCSS && IsBlockNode(curNode)) {
nsAtom& marginProperty = MarginPropertyAtomForIndent(curNode);
nsAutoString value;
CSSEditUtils::GetSpecifiedProperty(curNode, marginProperty, value);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
float f;
RefPtr<nsAtom> unit;
CSSEditUtils::ParseLength(value, &f, getter_AddRefs(unit));
if (f > 0) {
nsresult rv = DecreaseMarginToOutdent(*curNode->AsElement());
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to decrease indentation");
continue;
}
}
// Is it a list item?
if (HTMLEditUtils::IsListItem(curNode)) {
// If it is a list item, that means we are not outdenting whole list.
// So we need to finish up dealing with any curBlockQuote, and then pop
// this list item.
if (curBlockQuote) {
SplitRangeOffFromNodeResult outdentResult =
OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS);
if (NS_WARN_IF(outdentResult.Failed())) {
return outdentResult;
}
leftContentOfLastOutdented = outdentResult.GetLeftContent();
middleContentOfLastOutdented = outdentResult.GetMiddleContent();
rightContentOfLastOutdented = outdentResult.GetRightContent();
curBlockQuote = nullptr;
firstBQChild = nullptr;
lastBQChild = nullptr;
curBlockQuoteIsIndentedWithCSS = false;
}
rv = PopListItem(*curNode->AsContent());
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
continue;
}
// Do we have a blockquote that we are already committed to removing?
if (curBlockQuote) {
// If so, is this node a descendant?
if (EditorUtils::IsDescendantOf(*curNode, *curBlockQuote)) {
lastBQChild = curNode;
// Then we don't need to do anything different for this node
continue;
}
// Otherwise, we have progressed beyond end of curBlockQuote, so
// let's handle it now. We need to remove the portion of
// curBlockQuote that contains [firstBQChild - lastBQChild].
SplitRangeOffFromNodeResult outdentResult =
OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS);
if (NS_WARN_IF(outdentResult.Failed())) {
return outdentResult;
}
leftContentOfLastOutdented = outdentResult.GetLeftContent();
middleContentOfLastOutdented = outdentResult.GetMiddleContent();
rightContentOfLastOutdented = outdentResult.GetRightContent();
curBlockQuote = nullptr;
firstBQChild = nullptr;
lastBQChild = nullptr;
curBlockQuoteIsIndentedWithCSS = false;
// Fall out and handle curNode
}
// Are we inside a blockquote?
OwningNonNull<nsINode> n = curNode;
curBlockQuoteIsIndentedWithCSS = false;
// Keep looking up the hierarchy as long as we don't hit the body or the
// active editing host or a table element (other than an entire table)
while (!n->IsHTMLElement(nsGkAtoms::body) &&
HTMLEditorRef().IsDescendantOfEditorRoot(n) &&
(n->IsHTMLElement(nsGkAtoms::table) ||
!HTMLEditUtils::IsTableElement(n))) {
if (!n->GetParentNode()) {
break;
}
n = *n->GetParentNode();
if (n->IsHTMLElement(nsGkAtoms::blockquote)) {
// If so, remember it and the first node we are taking out of it.
curBlockQuote = n->AsElement();
firstBQChild = curNode;
lastBQChild = curNode;
break;
}
if (!useCSS) {
continue;
}
nsAtom& marginProperty = MarginPropertyAtomForIndent(curNode);
nsAutoString value;
CSSEditUtils::GetSpecifiedProperty(*n, marginProperty, value);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
float f;
RefPtr<nsAtom> unit;
CSSEditUtils::ParseLength(value, &f, getter_AddRefs(unit));
if (f > 0 && !(HTMLEditUtils::IsList(curParent) &&
HTMLEditUtils::IsList(curNode))) {
curBlockQuote = n->AsElement();
firstBQChild = curNode;
lastBQChild = curNode;
curBlockQuoteIsIndentedWithCSS = true;
break;
}
}
if (curBlockQuote) {
continue;
}
// Couldn't find enclosing blockquote.
if (HTMLEditUtils::IsList(curParent)) {
// Move node out of list
if (HTMLEditUtils::IsList(curNode)) {
// Just unwrap this sublist
rv = HTMLEditorRef().RemoveBlockContainerWithTransaction(
*curNode->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
}
continue;
}
if (HTMLEditUtils::IsList(curNode)) {
// node is a list, but parent is non-list: move list items out
nsCOMPtr<nsIContent> child = curNode->GetLastChild();
while (child) {
if (HTMLEditUtils::IsListItem(child)) {
rv = PopListItem(*child);
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
} else if (HTMLEditUtils::IsList(child)) {
// We have an embedded list, so move it out from under the parent
// list. Be sure to put it after the parent list because this
// loop iterates backwards through the parent's list of children.
EditorRawDOMPoint afterCurrentList(curParent, offset + 1);
rv =
HTMLEditorRef().MoveNodeWithTransaction(*child, afterCurrentList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
} else {
// Delete any non-list items for now
rv = HTMLEditorRef().DeleteNodeWithTransaction(*child);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
}
child = curNode->GetLastChild();
}
// Delete the now-empty list
rv = HTMLEditorRef().RemoveBlockContainerWithTransaction(
*curNode->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
continue;
}
if (useCSS) {
nsCOMPtr<Element> element;
if (curNode->GetAsText()) {
// We want to outdent the parent of text nodes
element = curNode->GetParentElement();
} else if (curNode->IsElement()) {
element = curNode->AsElement();
}
if (element) {
nsresult rv = DecreaseMarginToOutdent(*element);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to decrease indentation");
}
continue;
}
}
if (!curBlockQuote) {
return SplitRangeOffFromNodeResult(leftContentOfLastOutdented,
middleContentOfLastOutdented,
rightContentOfLastOutdented);
}
// We have a <blockquote> we haven't finished handling.
SplitRangeOffFromNodeResult outdentResult =
OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS);
if (NS_WARN_IF(outdentResult.Failed())) {
return outdentResult;
}
return outdentResult;
}
SplitRangeOffFromNodeResult
HTMLEditRules::SplitRangeOffFromBlockAndRemoveMiddleContainer(
Element& aBlockElement, nsIContent& aStartOfRange,
nsIContent& aEndOfRange) {
MOZ_ASSERT(IsEditorDataAvailable());
SplitRangeOffFromNodeResult splitResult =
SplitRangeOffFromBlock(aBlockElement, aStartOfRange, aEndOfRange);
if (NS_WARN_IF(splitResult.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return splitResult;
}
NS_WARNING_ASSERTION(splitResult.Succeeded(),
"Failed to split the range off from the block element");
nsresult rv =
HTMLEditorRef().RemoveBlockContainerWithTransaction(aBlockElement);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
return SplitRangeOffFromNodeResult(splitResult.GetLeftContent(), nullptr,
splitResult.GetRightContent());
}
SplitRangeOffFromNodeResult HTMLEditRules::SplitRangeOffFromBlock(
Element& aBlockElement, nsIContent& aStartOfMiddleElement,
nsIContent& aEndOfMiddleElement) {
MOZ_ASSERT(IsEditorDataAvailable());
// aStartOfMiddleElement and aEndOfMiddleElement must be exclusive
// descendants of aBlockElement.
MOZ_ASSERT(EditorUtils::IsDescendantOf(aStartOfMiddleElement, aBlockElement));
MOZ_ASSERT(EditorUtils::IsDescendantOf(aEndOfMiddleElement, aBlockElement));
// Split at the start.
SplitNodeResult splitAtStartResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
aBlockElement, EditorRawDOMPoint(&aStartOfMiddleElement),
SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(splitAtStartResult.Succeeded(),
"Failed to split aBlockElement at start");
// Split at after the end
EditorRawDOMPoint atAfterEnd(&aEndOfMiddleElement);
DebugOnly<bool> advanced = atAfterEnd.AdvanceOffset();
NS_WARNING_ASSERTION(advanced, "Failed to advance offset after the end node");
SplitNodeResult splitAtEndResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
aBlockElement, atAfterEnd, SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(splitAtEndResult.Succeeded(),
"Failed to split aBlockElement at after end");
return SplitRangeOffFromNodeResult(splitAtStartResult, splitAtEndResult);
}
SplitRangeOffFromNodeResult HTMLEditRules::OutdentPartOfBlock(
Element& aBlockElement, nsIContent& aStartOfOutdent,
nsIContent& aEndOfOutdent, bool aIsBlockIndentedWithCSS) {
MOZ_ASSERT(IsEditorDataAvailable());
SplitRangeOffFromNodeResult splitResult =
SplitRangeOffFromBlock(aBlockElement, aStartOfOutdent, aEndOfOutdent);
if (NS_WARN_IF(splitResult.Rv() == NS_ERROR_EDITOR_DESTROYED)) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(!splitResult.GetMiddleContentAsElement())) {
return SplitRangeOffFromNodeResult(NS_ERROR_FAILURE);
}
if (!aIsBlockIndentedWithCSS) {
nsresult rv = HTMLEditorRef().RemoveBlockContainerWithTransaction(
*splitResult.GetMiddleContentAsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitRangeOffFromNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
return SplitRangeOffFromNodeResult(splitResult.GetLeftContent(), nullptr,
splitResult.GetRightContent());
}
if (splitResult.GetMiddleContentAsElement()) {
nsresult rv =
DecreaseMarginToOutdent(*splitResult.GetMiddleContentAsElement());
if (NS_WARN_IF(NS_FAILED(rv))) {
return SplitRangeOffFromNodeResult(rv);
}
return splitResult;
}
return splitResult;
}
CreateElementResult HTMLEditRules::ConvertListType(Element& aListElement,
nsAtom& aNewListTag,
nsAtom& aNewListItemTag) {
MOZ_ASSERT(IsEditorDataAvailable());
nsCOMPtr<nsINode> child = aListElement.GetFirstChild();
while (child) {
if (child->IsElement()) {
Element* element = child->AsElement();
if (HTMLEditUtils::IsListItem(element) &&
!element->IsHTMLElement(&aNewListItemTag)) {
child = HTMLEditorRef().ReplaceContainerWithTransaction(
*element, aNewListItemTag);
if (NS_WARN_IF(!CanHandleEditAction())) {
return CreateElementResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(!child)) {
return CreateElementResult(NS_ERROR_FAILURE);
}
} else if (HTMLEditUtils::IsList(element) &&
!element->IsHTMLElement(&aNewListTag)) {
// XXX List elements shouldn't have other list elements as their
// child. Why do we handle such invalid tree?
CreateElementResult convertListTypeResult =
ConvertListType(*child->AsElement(), aNewListTag, aNewListItemTag);
if (NS_WARN_IF(convertListTypeResult.Failed())) {
return convertListTypeResult;
}
child = convertListTypeResult.forget();
}
}
child = child->GetNextSibling();
}
if (aListElement.IsHTMLElement(&aNewListTag)) {
return CreateElementResult(&aListElement);
}
RefPtr<Element> listElement = HTMLEditorRef().ReplaceContainerWithTransaction(
aListElement, aNewListTag);
if (NS_WARN_IF(!CanHandleEditAction())) {
return CreateElementResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(listElement != nullptr, "Failed to create list element");
return CreateElementResult(listElement.forget());
}
nsresult HTMLEditRules::CreateStyleForInsertText(Document& aDocument) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(HTMLEditorRef().mTypeInState);
bool weDidSomething = false;
nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsINode> node = firstRange->GetStartContainer();
int32_t offset = firstRange->StartOffset();
RefPtr<Element> rootElement = aDocument.GetRootElement();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_FAILURE;
}
// process clearing any styles first
UniquePtr<PropItem> item = HTMLEditorRef().mTypeInState->TakeClearProperty();
{
// Transactions may set selection, but we will set selection if necessary.
AutoTransactionsConserveSelection dontChangeMySelection(HTMLEditorRef());
while (item && node != rootElement) {
// XXX If we redesign ClearStyle(), we can use EditorDOMPoint in this
// method.
nsresult rv = HTMLEditorRef().ClearStyle(address_of(node), &offset,
item->tag, item->attr);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
item = HTMLEditorRef().mTypeInState->TakeClearProperty();
weDidSomething = true;
}
}
// then process setting any styles
int32_t relFontSize = HTMLEditorRef().mTypeInState->TakeRelativeFontSize();
item = HTMLEditorRef().mTypeInState->TakeSetProperty();
if (item || relFontSize) {
// we have at least one style to add; make a new text node to insert style
// nodes above.
if (RefPtr<Text> text = node->GetAsText()) {
// if we are in a text node, split it
SplitNodeResult splitTextNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*text, EditorRawDOMPoint(text, offset),
SplitAtEdges::eAllowToCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitTextNodeResult.Failed())) {
return splitTextNodeResult.Rv();
}
EditorRawDOMPoint splitPoint(splitTextNodeResult.SplitPoint());
node = splitPoint.GetContainer();
offset = splitPoint.Offset();
}
if (!HTMLEditorRef().IsContainer(node)) {
return NS_OK;
}
OwningNonNull<Text> newNode =
EditorBase::CreateTextNode(aDocument, EmptyString());
nsresult rv = HTMLEditorRef().InsertNodeWithTransaction(
*newNode, EditorRawDOMPoint(node, offset));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
node = newNode;
offset = 0;
weDidSomething = true;
if (relFontSize) {
// dir indicated bigger versus smaller. 1 = bigger, -1 = smaller
HTMLEditor::FontSize dir = relFontSize > 0 ? HTMLEditor::FontSize::incr
: HTMLEditor::FontSize::decr;
for (int32_t j = 0; j < DeprecatedAbs(relFontSize); j++) {
rv = HTMLEditorRef().RelativeFontChangeOnTextNode(dir, newNode, 0, -1);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
while (item) {
rv = HTMLEditorRef().SetInlinePropertyOnNode(
*node->AsContent(), *item->tag, item->attr, item->value);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
item = HTMLEditorRef().mTypeInState->TakeSetProperty();
}
}
if (!weDidSomething) {
return NS_OK;
}
nsresult rv = SelectionRefPtr()->Collapse(node, offset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
bool HTMLEditRules::IsEmptyBlockElement(Element& aElement,
IgnoreSingleBR aIgnoreSingleBR) {
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!IsBlockNode(aElement))) {
return false;
}
bool isEmpty = true;
nsresult rv = HTMLEditorRef().IsEmptyNode(
&aElement, &isEmpty, aIgnoreSingleBR == IgnoreSingleBR::eYes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
return isEmpty;
}
nsresult HTMLEditRules::WillAlign(const nsAString& aAlignType, bool* aCancel,
bool* aHandled) {
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aCancel && aHandled);
*aCancel = false;
*aHandled = false;
// FYI: Ignore cancel result of WillInsert().
nsresult rv = WillInsert();
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "WillInsert() failed");
rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
*aHandled = true;
rv = AlignContentsAtSelection(aAlignType);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED) ||
NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult HTMLEditRules::AlignContentsAtSelection(const nsAString& aAlignType) {
AutoSelectionRestorer restoreSelectionLater(HTMLEditorRef());
// Convert the selection ranges into "promoted" selection ranges: This
// basically just expands the range to include the immediate block parent,
// and then further expands to include any ancestors whose children are all
// in the range
nsTArray<OwningNonNull<nsINode>> nodeArray;
nsresult rv = GetNodesFromSelection(EditSubAction::eSetOrClearAlignment,
nodeArray, TouchContent::yes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// If we don't have any nodes, or we have only a single br, then we are
// creating an empty alignment div. We have to do some different things for
// these.
bool emptyDiv = nodeArray.IsEmpty();
if (nodeArray.Length() == 1) {
OwningNonNull<nsINode> node = nodeArray[0];
if (HTMLEditUtils::SupportsAlignAttr(*node)) {
// The node is a table element, an hr, a paragraph, a div or a section
// header; in HTML 4, it can directly carry the ALIGN attribute and we
// don't need to make a div! If we are in CSS mode, all the work is done
// in AlignBlock
rv =