gecko-dev/editor/libeditor/HTMLEditRules.cpp
Masayuki Nakano 98a833407e Bug 1467794 - Split TextEditor::DeleteSelectionAsAction() to itself and TextEditor::DeleteSelectionAsSubAction() r=m_kato
TextEditor::DeleteSelectionAsAction() is called even if it's a part of edit
action.  For example, it's called to prepare for inserting text.

For bug 1465702, editor itself and edit rules classes should not call
public DeleteSelectionAsAction() directly.  Therefore, this patch creates
DeleteSelectionAsSubAction() for internal use.

Note that this patch adds NS_ASSERTION() to detect wrong caller.  However,
it cannot distinguish if the call is valid, for example, it's allowed to
call DeleteSelectionAsSelection() even if it's handling an edit action but
the method is called via mutation event listener.  So, we need to allow
some assertions with some tests.  But unfortunately, 1405747.html uses
mutation event listener too many times (about 1,000 times) and the number
of assertion isn't stable.  Therefore, this patch makes the test stop using
the mutation event listener 2nd time since I can reproduce the crash with
ESR 52 at the 2nd time.

MozReview-Commit-ID: 1TWaypmnoCC

--HG--
extra : rebase_source : a6a4fb1cbcaf2ab6f10c5f3e7168a6bc0fcb02ed
2018-06-29 20:16:50 +09:00

11385 lines
386 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=79: */
/* 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/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 "nsIContentIterator.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::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;
};
/********************************************************
* 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;
}
Selection* selection = aTextEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
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;
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
// Remember where our selection was before edit action took place:
// Get the selection location
if (!SelectionRef().RangeCount()) {
return NS_ERROR_UNEXPECTED;
}
mRangeItem->StoreRange(SelectionRef().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().mRangeUpdater.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) {
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
// 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().mRangeUpdater.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::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::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(&SelectionRef());
rv = ReapplyCachedStyles();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
ClearCachedStyles();
}
}
rv = HTMLEditorRef().HandleInlineSpellCheck(aEditSubAction, SelectionRef(),
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(Selection* aSelection,
EditSubActionInfo& aInfo,
bool* aCancel,
bool* aHandled)
{
if (NS_WARN_IF(!aSelection)) {
return NS_ERROR_INVALID_ARG;
}
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(aSelection, aInfo, aCancel, aHandled);
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *aSelection);
// Nothing to do if there's no selection to act on
if (NS_WARN_IF(!SelectionRef().RangeCount())) {
return NS_OK;
}
RefPtr<nsRange> range = SelectionRef().GetRangeAt(0);
nsCOMPtr<nsINode> selStartNode = range->GetStartContainer();
if (!HTMLEditorRef().IsModifiableNode(selStartNode)) {
*aCancel = true;
return NS_OK;
}
nsCOMPtr<nsINode> selEndNode = range->GetEndContainer();
if (selStartNode != selEndNode) {
if (!HTMLEditorRef().IsModifiableNode(selEndNode)) {
*aCancel = true;
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
if (!HTMLEditorRef().IsModifiableNode(range->GetCommonAncestor())) {
*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();
return WillInsertBreak(aCancel, aHandled);
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(&SelectionRef(), aInfo,
aCancel, aHandled);
}
}
nsresult
HTMLEditRules::DidDoAction(Selection* aSelection,
EditSubActionInfo& aInfo,
nsresult aResult)
{
if (NS_WARN_IF(!aSelection)) {
return NS_ERROR_INVALID_ARG;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *aSelection);
switch (aInfo.mEditSubAction) {
case EditSubAction::eInsertText:
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(aSelection, 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;
}
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
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;
}
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
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;
}
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
// 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 = SelectionRef().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 (SelectionRef().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);
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
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(&SelectionRef()));
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 (!SelectionRef().IsCollapsed()) {
return NS_OK;
}
// If we are after a mozBR in the same block, then move selection to be
// before it
nsRange* firstRange = SelectionRef().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;
SelectionRef().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 (!SelectionRef().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
nsCOMPtr<nsIDocument> 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 = SelectionRef().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().mRangeUpdater, &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(SelectionRef(),
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(SelectionRef(), 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;
SelectionRef().SetInterlinePosition(false, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to unset interline position");
if (currentPoint.IsSet()) {
IgnoredErrorResult ignoredError;
SelectionRef().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;
}
nsresult
HTMLEditRules::WillInsertBreak(bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aCancel && aHandled);
*aCancel = false;
*aHandled = false;
// If the selection isn't collapsed, delete it.
if (!SelectionRef().IsCollapsed()) {
nsresult rv =
HTMLEditorRef().DeleteSelectionAsSubAction(nsIEditor::eNone,
nsIEditor::eStrip);
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");
// Split any mailcites in the way. Should we abort this if we encounter
// table cell boundaries?
if (IsMailEditor()) {
nsresult rv = SplitMailCites(aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (*aHandled) {
return NS_OK;
}
}
// Smart splitting rules
nsRange* firstRange = SelectionRef().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;
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
// Do nothing if the node is read-only
if (!HTMLEditorRef().IsModifiableNode(atStartOfSelection.GetContainer())) {
*aCancel = true;
return NS_OK;
}
// 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 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 rv;
}
*aHandled = true;
return NS_OK;
}
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 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 = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
atStartOfSelection = firstRange->StartRef();
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
blockParent =
HTMLEditor::GetBlock(*atStartOfSelection.GetContainer(), host);
if (NS_WARN_IF(!blockParent)) {
return 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 rv;
}
*aHandled = true;
return NS_OK;
}
// 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(SelectionRef(),
endOfBlockParent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return 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 NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to insert break into list item");
*aHandled = true;
return NS_OK;
}
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 NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to handle insertParagraph in the heading element");
*aHandled = true;
return NS_OK;
}
// 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.Rv();
}
*aHandled = result.Handled();
*aCancel = result.Canceled();
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 NS_OK;
}
// Fall through, if ReturnInParagraph() didn't handle it.
MOZ_ASSERT(!*aCancel, "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.
MOZ_ASSERT(!*aHandled, "Reached last resort of WillInsertBreak() "
"after the edit action is handled");
rv = InsertBRElement(atStartOfSelection);
*aHandled = true;
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
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(SelectionRef(),
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);
int32_t visOffset = 0;
WSType wsType;
nsCOMPtr<nsINode> visNode;
wsObj.PriorVisibleNode(pointToBreak,
address_of(visNode), &visOffset, &wsType);
if (wsType & WSType::block) {
brElementIsAfterBlock = true;
}
wsObj.NextVisibleNode(pointToBreak,
address_of(visNode), &visOffset, &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.
nsCOMPtr<nsINode> linkDOMNode;
if (HTMLEditorRef().IsInLink(pointToBreak.GetContainer(),
address_of(linkDOMNode))) {
nsCOMPtr<Element> linkNode = do_QueryInterface(linkDOMNode);
if (NS_WARN_IF(!linkNode)) {
return NS_ERROR_FAILURE;
}
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(SelectionRef(), 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;
SelectionRef().SetInterlinePosition(true, error);
NS_WARNING_ASSERTION(!error.Failed(), "Failed to set interline position");
EditorRawDOMPoint point(brElement);
error = NS_OK;
SelectionRef().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;
int32_t visOffset = 0;
WSType wsType;
wsObj.NextVisibleNode(afterBRElement,
address_of(maybeSecondBRNode), &visOffset, &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;
SelectionRef().SetInterlinePosition(!(nextSiblingOfBRElement &&
IsBlockNode(*nextSiblingOfBRElement)),
error);
NS_WARNING_ASSERTION(!error.Failed(),
"Failed to set or unset interline position");
error = NS_OK;
SelectionRef().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;
}
nsresult
HTMLEditRules::SplitMailCites(bool* aHandled)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aHandled)) {
return NS_ERROR_INVALID_ARG;
}
EditorRawDOMPoint pointToSplit(EditorBase::GetStartPoint(&SelectionRef()));
if (NS_WARN_IF(!pointToSplit.IsSet())) {
return NS_ERROR_FAILURE;
}
RefPtr<Element> citeNode =
GetTopEnclosingMailCite(*pointToSplit.GetContainer());
if (!citeNode) {
return NS_OK;
}
// 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;
int32_t visOffset=0;
WSType wsType;
wsObj.NextVisibleNode(pointToSplit, address_of(visNode), &visOffset, &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 NS_ERROR_FAILURE;
}
SplitNodeResult splitCiteNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*citeNode, pointToSplit,
SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitCiteNodeResult.Failed())) {
return 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()->
IsFrameOfType(nsIFrame::eBlockFrame)) {
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(
SelectionRef(),
endOfPreviousNodeOfSplitPoint);
if (NS_WARN_IF(!CanHandleEditAction())) {
return 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(SelectionRef(),
pointToInsertBrNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return 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;
SelectionRef().SetInterlinePosition(true, error);
NS_WARNING_ASSERTION(!error.Failed(),
"Failed to set interline position");
error = NS_OK;
SelectionRef().Collapse(atBrNode, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return 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);
nsCOMPtr<nsINode> visNode;
int32_t visOffset=0;
WSType wsType;
wsObj.PriorVisibleNode(pointToCreateNewBrNode,
address_of(visNode), &visOffset, &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,
address_of(visNode), &visOffset, &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(
SelectionRef(), pointToCreateNewBrNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return 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 rv;
}
if (bEmptyCite) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*previousNodeOfSplitPoint);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
if (citeNode) {
nsresult rv =
HTMLEditorRef().IsEmptyNode(citeNode, &bEmptyCite, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (bEmptyCite) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*citeNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
*aHandled = true;
return NS_OK;
}
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.
RefPtr<Element> cell;
nsresult rv =
HTMLEditorRef().GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
if (NS_SUCCEEDED(rv) && cell) {
rv = HTMLEditorRef().DeleteTableCellContents();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
*aHandled = true;
return rv;
}
cell = 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 bCollapsed = SelectionRef().IsCollapsed();
bool join = false;
bool origCollapsed = bCollapsed;
nsCOMPtr<nsINode> selNode;
int32_t selOffset;
nsRange* firstRange = SelectionRef().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();
if (bCollapsed) {
// If we are inside an empty block, delete it.
RefPtr<Element> host = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!host)) {
return NS_ERROR_FAILURE;
}
rv = MaybeDeleteTopMostEmptyAncestor(*startNode, *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(EditorRawDOMPoint(startNode, startOffset),
aAction, aCancel);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (*aCancel) {
return NS_OK;
}
rv = HTMLEditorRef().ExtendSelectionForDelete(&SelectionRef(), &aAction);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We should delete nothing.
if (aAction == nsIEditor::eNone) {
return NS_OK;
}
// ExtendSelectionForDelete() may have changed the selection, update it
firstRange = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
startNode = firstRange->GetStartContainer();
if (NS_WARN_IF(!startNode)) {
return NS_ERROR_FAILURE;
}
startOffset = firstRange->StartOffset();
bCollapsed = SelectionRef().IsCollapsed();
}
if (bCollapsed) {
// What's in the direction we are deleting?
WSRunObject wsObj(&HTMLEditorRef(), startNode, startOffset);
nsCOMPtr<nsINode> visNode;
int32_t visOffset;
WSType wsType;
// Find next visible node
if (aAction == nsIEditor::eNext) {
wsObj.NextVisibleNode(EditorRawDOMPoint(startNode, startOffset),
address_of(visNode), &visOffset, &wsType);
} else {
wsObj.PriorVisibleNode(EditorRawDOMPoint(startNode, startOffset),
address_of(visNode), &visOffset, &wsType);
}
if (!visNode) {
// Can't find anything to delete!
*aCancel = true;
// XXX This is the result of HTMLEditorRef().GetFirstSelectedCell().
// 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 = SelectionRef().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 startNode and startOffset) is the position directly
// after the <hr>, on the same line as the <hr>.
//
// To detect this case we check:
// startNode == parentOfVisNode
// and
// startOffset -1 == visNodeOffsetToVisNodeParent
// and
// interline position is false (left)
//
// In any other case we set the position to startnode -1 and
// interlineposition to false, only moving the caret to the
// end-of-hr-line position.
bool moveOnly = true;
selNode = visNode->GetParentNode();
selOffset = selNode ? selNode->ComputeIndexOf(visNode) : -1;
ErrorResult err;
bool interLineIsRight = SelectionRef().GetInterlinePosition(err);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}
if (startNode == selNode && startOffset - 1 == selOffset &&
!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.
++selOffset;
IgnoredErrorResult ignoredError;
SelectionRef().Collapse(RawRangeBoundary(selNode, selOffset),
ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to collapse selection at after the <hr>");
(ErrorResult&)ignoredError = NS_OK;
SelectionRef().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;
int32_t otherOffset;
wsObj.NextVisibleNode(EditorRawDOMPoint(startNode, startOffset),
address_of(otherNode),
&otherOffset, &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 (startNode == stepbrother && startNode->GetAsText() &&
sibling->GetAsText()) {
EditorDOMPoint pt;
nsresult rv =
JoinNearestEditableNodesWithTransaction(*sibling,
*startNode->AsContent(), &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;
SelectionRef().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;
int32_t otherOffset;
// Find node in other direction
if (aAction == nsIEditor::eNext) {
wsObj.PriorVisibleNode(EditorRawDOMPoint(startNode, startOffset),
address_of(otherNode),
&otherOffset, &otherWSType);
} else {
wsObj.NextVisibleNode(EditorRawDOMPoint(startNode, startOffset),
address_of(otherNode),
&otherOffset, &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 = startNode;
} else {
leafNode = HTMLEditorRef().GetFirstEditableLeaf(*visNode);
leftNode = startNode;
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;
SelectionRef().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
nsCOMPtr<nsINode> selPointNode = startNode;
int32_t selPointOffset = startOffset;
{
AutoTrackDOMPoint tracker(HTMLEditorRef().mRangeUpdater,
address_of(selPointNode), &selPointOffset);
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 != startNode) {
int32_t offset =
aAction == nsIEditor::ePrevious ?
static_cast<int32_t>(leafNode->Length()) : 0;
rv = SelectionRef().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.
rv = SelectionRef().Collapse(selPointNode, selPointOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"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 = startNode;
} else {
rightNode = HTMLEditorRef().GetNextEditableHTMLNode(*visNode);
leftNode = startNode;
}
// 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;
}
nsCOMPtr<nsINode> selPointNode = startNode;
int32_t selPointOffset = startOffset;
{
AutoTrackDOMPoint tracker(HTMLEditorRef().mRangeUpdater,
address_of(selPointNode), &selPointOffset);
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();
}
}
rv = SelectionRef().Collapse(selPointNode, selPointOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "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
firstRange = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
startNode = firstRange->GetStartContainer();
if (NS_WARN_IF(!startNode)) {
return NS_ERROR_FAILURE;
}
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().mRangeUpdater,
address_of(startNode), &startOffset);
AutoTrackDOMPoint endTracker(HTMLEditorRef().mRangeUpdater,
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;
SelectionRef().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(&SelectionRef());
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().mRangeUpdater,
address_of(startNode), &startOffset);
AutoTrackDOMPoint endTracker(HTMLEditorRef().mRangeUpdater,
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 = SelectionRef().Collapse(endNode, endOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = SelectionRef().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(&SelectionRef()));
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(SelectionRef(),
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().mRangeUpdater,
&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().mRangeUpdater, &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(&SelectionRef()));
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(SelectionRef(),
atCiteNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
IgnoredErrorResult error;
SelectionRef().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 selectionRestorer(&SelectionRef(), &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 = SelectionRef().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.
selectionRestorer.Abort();
ErrorResult error;
SelectionRef().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 selectionRestorer(&SelectionRef(), &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 selectionRestorer(&SelectionRef(), &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 = SelectionRef().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(SelectionRef(),
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
selectionRestorer.Abort();
ErrorResult error;
SelectionRef().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
selectionRestorer.Abort();
// Put selection in new block
rv = SelectionRef().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 (!SelectionRef().IsCollapsed()) {
return NS_OK;
}
nsRange* firstRange = SelectionRef().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 selectionRestorer(&SelectionRef(), &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 (SelectionRef().IsCollapsed()) {
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(&SelectionRef()));
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 = SelectionRef().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
selectionRestorer.Abort();
ErrorResult error;
SelectionRef().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 selectionRestorer(&SelectionRef(), &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 = SelectionRef().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
selectionRestorer.Abort();
ErrorResult error;
SelectionRef().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 (!SelectionRef().IsCollapsed()) {
return NS_OK;
}
// Push selection past end of left element of last split indented element.
if (outdentResult.GetLeftContent()) {
nsRange* firstRange = SelectionRef().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;
SelectionRef().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 = SelectionRef().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;
SelectionRef().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 selectionRestorer(&SelectionRef(), &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(nsIDocument& aDocument)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(HTMLEditorRef().mTypeInState);
bool weDidSomething = false;
nsRange* firstRange = SelectionRef().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 = SelectionRef().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 selectionRestorer(&SelectionRef(), &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 = AlignBlock(*node->AsElement(), aAlignType,
ResetAlignOf::OnlyDescendants);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (TextEditUtils::IsBreak(node)) {
// The special case emptyDiv code (below) that consumes BRs can cause
// tables to split if the start node of the selection is not in a table
// cell or caption, for example parent is a <tr>. Avoid this unnecessary
// splitting if possible by leaving emptyDiv FALSE so that we fall
// through to the normal case alignment code.
//
// XXX: It seems a little error prone for the emptyDiv special case code
// to assume that the start node of the selection is the parent of the
// single node in the nodeArray, as the paragraph above points out. Do we
// rely on the selection start node because of the fact that nodeArray
// can be empty? We should probably revisit this issue. - kin
nsRange* firstRange = SelectionRef().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;
}
nsINode* parent = atStartOfSelection.Container();
emptyDiv = !HTMLEditUtils::IsTableElement(parent) ||
HTMLEditUtils::IsTableCellOrCaption(*parent);
}
}
if (emptyDiv) {
nsRange* firstRange = SelectionRef().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;
}
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div,
atStartOfSelection);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
// Consume a trailing br, if any. This is to keep an alignment from
// creating extra lines, if possible.
nsCOMPtr<nsIContent> brContent =
HTMLEditorRef().GetNextEditableHTMLNodeInBlock(
splitNodeResult.SplitPoint());
EditorDOMPoint pointToInsertDiv(splitNodeResult.SplitPoint());
if (brContent && TextEditUtils::IsBreak(brContent)) {
// Making use of html structure... if next node after where we are
// putting our div is not a block, then the br we found is in same block
// we are, so it's safe to consume it.
nsCOMPtr<nsIContent> sibling;
if (pointToInsertDiv.GetChild()) {
sibling =
HTMLEditorRef().GetNextHTMLSibling(pointToInsertDiv.GetChild());
}
if (sibling && !IsBlockNode(*sibling)) {
AutoEditorDOMPointChildInvalidator lockOffset(pointToInsertDiv);
rv = HTMLEditorRef().DeleteNodeWithTransaction(*brContent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
RefPtr<Element> div =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::div,
pointToInsertDiv);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!div)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
mNewBlock = div;
// Set up the alignment on the div, using HTML or CSS
rv = AlignBlock(*div, aAlignType, ResetAlignOf::OnlyDescendants);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Put in a moz-br so that it won't get deleted
CreateElementResult createMozBrResult =
CreateMozBR(EditorRawDOMPoint(div, 0));
if (NS_WARN_IF(createMozBrResult.Failed())) {
return createMozBrResult.Rv();
}
EditorRawDOMPoint atStartOfDiv(div, 0);
// Don't restore the selection
selectionRestorer.Abort();
ErrorResult error;
SelectionRef().Collapse(atStartOfDiv, 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;
}
// Next we detect all the transitions in the array, where a transition
// means that adjacent nodes in the array don't have the same parent.
nsTArray<bool> transitionList;
MakeTransitionList(nodeArray, transitionList);
// Okay, now go through all the nodes and give them an align attrib or put
// them in a div, or whatever is appropriate. Woohoo!
nsCOMPtr<Element> curDiv;
bool useCSS = HTMLEditorRef().IsCSSEnabled();
int32_t indexOfTransitionList = -1;
for (OwningNonNull<nsINode>& curNode : nodeArray) {
++indexOfTransitionList;
// Ignore all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(curNode)) {
continue;
}
// 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 nest it, just set the alignment. In CSS, assign the
// corresponding CSS styles in AlignBlock
if (HTMLEditUtils::SupportsAlignAttr(*curNode)) {
rv = AlignBlock(*curNode->AsElement(), aAlignType,
ResetAlignOf::ElementAndDescendants);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Clear out curDiv so that we don't put nodes after this one into it
curDiv = nullptr;
continue;
}
EditorDOMPoint atCurNode(curNode);
if (NS_WARN_IF(!atCurNode.IsSet())) {
continue;
}
// Skip insignificant formatting text nodes to prevent unnecessary
// structure splitting!
bool isEmptyTextNode = false;
if (curNode->GetAsText() &&
((HTMLEditUtils::IsTableElement(atCurNode.GetContainer()) &&
!HTMLEditUtils::IsTableCellOrCaption(*atCurNode.GetContainer())) ||
HTMLEditUtils::IsList(atCurNode.GetContainer()) ||
(NS_SUCCEEDED(HTMLEditorRef().IsEmptyNode(curNode,
&isEmptyTextNode)) &&
isEmptyTextNode))) {
continue;
}
// If it's a list item, or a list inside a list, forget any "current" div,
// and instead put divs inside the appropriate block (td, li, etc.)
if (HTMLEditUtils::IsListItem(curNode) ||
HTMLEditUtils::IsList(curNode)) {
AutoEditorDOMPointOffsetInvalidator lockChild(atCurNode);
rv = RemoveAlignment(*curNode, aAlignType, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (useCSS) {
HTMLEditorRef().mCSSEditUtils->SetCSSEquivalentToHTMLStyle(
curNode->AsElement(), nullptr,
nsGkAtoms::align, &aAlignType, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
curDiv = nullptr;
continue;
}
if (HTMLEditUtils::IsList(atCurNode.GetContainer())) {
// If we don't use CSS, add a content to list element: they have to
// be inside another list, i.e., >= second level of nesting.
// XXX AlignInnerBlocks() handles list item elements and table cells.
// Is it intentional to change alignment of nested other type
// descendants too?
rv = AlignInnerBlocks(*curNode, aAlignType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
curDiv = nullptr;
continue;
}
// Clear out curDiv so that we don't put nodes after this one into it
}
// Need to make a div to put things in if we haven't already, or if this
// node doesn't go in div we used earlier.
if (!curDiv || transitionList[indexOfTransitionList]) {
// First, check that our element can contain a div.
if (!HTMLEditorRef().CanContainTag(*atCurNode.GetContainer(),
*nsGkAtoms::div)) {
// Cancelled
return NS_OK;
}
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div, atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curDiv =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::div,
splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curDiv)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
mNewBlock = curDiv;
// Set up the alignment on the div
rv = AlignBlock(*curDiv, aAlignType, ResetAlignOf::OnlyDescendants);
if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to align the <div>");
}
// Tuck the node into the end of the active div
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curDiv);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::AlignInnerBlocks(nsINode& aNode,
const nsAString& aAlignType)
{
MOZ_ASSERT(IsEditorDataAvailable());
// Gather list of table cells or list items
nsTArray<OwningNonNull<nsINode>> nodeArray;
TableCellAndListItemFunctor functor;
DOMIterator iter(aNode);
iter.AppendList(functor, nodeArray);
// Now that we have the list, align their contents as requested
for (auto& node : nodeArray) {
nsresult rv = AlignBlockContents(*node, aAlignType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::AlignBlockContents(nsINode& aNode,
const nsAString& aAlignType)
{
MOZ_ASSERT(IsEditorDataAvailable());
nsCOMPtr<nsIContent> firstChild =
HTMLEditorRef().GetFirstEditableChild(aNode);
if (!firstChild) {
// this cell has no content, nothing to align
return NS_OK;
}
nsCOMPtr<nsIContent> lastChild = HTMLEditorRef().GetLastEditableChild(aNode);
if (firstChild == lastChild && firstChild->IsHTMLElement(nsGkAtoms::div)) {
// the cell already has a div containing all of its content: just
// act on this div.
nsresult rv =
HTMLEditorRef().SetAttributeOrEquivalent(firstChild->AsElement(),
nsGkAtoms::align,
aAlignType, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// else we need to put in a div, set the alignment, and toss in all the
// children
EditorRawDOMPoint atStartOfNode(&aNode, 0);
RefPtr<Element> divElem =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::div, atStartOfNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!divElem)) {
return NS_ERROR_FAILURE;
}
// set up the alignment on the div
nsresult rv =
HTMLEditorRef().SetAttributeOrEquivalent(divElem, nsGkAtoms::align,
aAlignType, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// tuck the children into the end of the active div
while (lastChild && (lastChild != divElem)) {
nsresult rv =
HTMLEditorRef().MoveNodeWithTransaction(*lastChild,
EditorRawDOMPoint(divElem, 0));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
lastChild = HTMLEditorRef().GetLastEditableChild(aNode);
}
return NS_OK;
}
nsresult
HTMLEditRules::MaybeDeleteTopMostEmptyAncestor(nsINode& aStartNode,
Element& aEditingHostElement,
nsIEditor::EDirection aAction,
bool* aHandled)
{
MOZ_ASSERT(IsEditorDataAvailable());
// If the editing host is an inline element, bail out early.
if (IsInlineNode(aEditingHostElement)) {
return NS_OK;
}
// If we are inside an empty block, delete it. Note: do NOT delete table
// elements this way.
RefPtr<Element> block = HTMLEditorRef().GetBlock(aStartNode);
RefPtr<Element> emptyBlock;
if (block && block != &aEditingHostElement) {
// Efficiency hack, avoiding IsEmptyNode() call when in body
bool isEmptyNode = false;
nsresult rv =
HTMLEditorRef().IsEmptyNode(block, &isEmptyNode, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
while (block && isEmptyNode && !HTMLEditUtils::IsTableElement(block) &&
block != &aEditingHostElement) {
emptyBlock = block;
block = HTMLEditorRef().GetBlockNodeParent(emptyBlock);
if (block) {
rv = HTMLEditorRef().IsEmptyNode(block, &isEmptyNode, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
}
if (!emptyBlock || !emptyBlock->IsEditable()) {
return NS_OK;
}
nsCOMPtr<nsINode> blockParent = emptyBlock->GetParentNode();
if (NS_WARN_IF(!blockParent)) {
return NS_ERROR_FAILURE;
}
if (HTMLEditUtils::IsListItem(emptyBlock)) {
// If the found empty block is a list item element and its grand parent
// (i.e., parent of list element) is NOT a list element, insert <br>
// element before the list element which has the empty list item.
// XXX Typically, list element shouldn't have another list element.
// So, what's the purpose of this block?
if (HTMLEditorRef().IsFirstEditableChild(emptyBlock)) {
EditorDOMPoint atBlockParent(blockParent);
if (NS_WARN_IF(!atBlockParent.IsSet())) {
return NS_ERROR_FAILURE;
}
// If the grand parent IS a list element, we'll adjust Selection in
// AfterEdit().
if (!HTMLEditUtils::IsList(atBlockParent.GetContainer())) {
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(SelectionRef(),
atBlockParent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
SelectionRef().Collapse(EditorRawDOMPoint(brElement), error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
}
} else {
switch (aAction) {
case nsIEditor::eNext:
case nsIEditor::eNextWord:
case nsIEditor::eToEndOfLine: {
// Collapse Selection to next node of after empty block element
// if there is. Otherwise, to just after the empty block.
EditorRawDOMPoint afterEmptyBlock(emptyBlock);
bool advancedFromEmptyBlock = afterEmptyBlock.AdvanceOffset();
NS_WARNING_ASSERTION(advancedFromEmptyBlock,
"Failed to set selection to the after the empty block");
nsCOMPtr<nsIContent> nextNode =
HTMLEditorRef().GetNextNode(afterEmptyBlock);
if (nextNode) {
EditorDOMPoint pt = GetGoodSelPointForNode(*nextNode, aAction);
ErrorResult error;
SelectionRef().Collapse(pt, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
break;
}
if (NS_WARN_IF(!advancedFromEmptyBlock)) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
SelectionRef().Collapse(afterEmptyBlock, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
break;
}
case nsIEditor::ePrevious:
case nsIEditor::ePreviousWord:
case nsIEditor::eToBeginningOfLine: {
// Collapse Selection to previous editable node of the empty block
// if there is. Otherwise, to after the empty block.
EditorRawDOMPoint atEmptyBlock(emptyBlock);
nsCOMPtr<nsIContent> priorNode =
HTMLEditorRef().GetPreviousEditableNode(atEmptyBlock);
if (priorNode) {
EditorDOMPoint pt = GetGoodSelPointForNode(*priorNode, aAction);
ErrorResult error;
SelectionRef().Collapse(pt, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
break;
}
EditorRawDOMPoint afterEmptyBlock(emptyBlock);
if (NS_WARN_IF(!afterEmptyBlock.AdvanceOffset())) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
SelectionRef().Collapse(afterEmptyBlock, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
break;
}
case nsIEditor::eNone:
break;
default:
MOZ_CRASH("CheckForEmptyBlock doesn't support this action yet");
}
}
*aHandled = true;
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(*emptyBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
Element*
HTMLEditRules::CheckForInvisibleBR(Element& aBlock,
BRLocation aWhere,
int32_t aOffset)
{
MOZ_ASSERT(IsEditorDataAvailable());
nsCOMPtr<nsINode> testNode;
int32_t testOffset = 0;
if (aWhere == BRLocation::blockEnd) {
// No block crossing
nsCOMPtr<nsIContent> rightmostNode =
HTMLEditorRef().GetRightmostChild(&aBlock, true);
if (!rightmostNode) {
return nullptr;
}
testNode = rightmostNode->GetParentNode();
// Since rightmostNode is always the last child, its index is equal to the
// child count, so instead of ComputeIndexOf() we use the faster
// GetChildCount(), and assert the equivalence below.
testOffset = testNode->GetChildCount();
// Use offset + 1, so last node is included in our evaluation
MOZ_ASSERT(testNode->ComputeIndexOf(rightmostNode) + 1 == testOffset);
} else if (aOffset) {
testNode = &aBlock;
// We'll check everything to the left of the input position
testOffset = aOffset;
} else {
return nullptr;
}
WSRunObject wsTester(&HTMLEditorRef(), testNode, testOffset);
if (WSType::br == wsTester.mStartReason) {
return wsTester.mStartReasonNode->AsElement();
}
return nullptr;
}
void
HTMLEditRules::GetInnerContent(
nsINode& aNode,
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
int32_t* aIndex,
Lists aLists,
Tables aTables)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aIndex);
for (nsCOMPtr<nsIContent> node = HTMLEditorRef().GetFirstEditableChild(aNode);
node; node = node->GetNextSibling()) {
if ((aLists == Lists::yes && (HTMLEditUtils::IsList(node) ||
HTMLEditUtils::IsListItem(node))) ||
(aTables == Tables::yes && HTMLEditUtils::IsTableElement(node))) {
GetInnerContent(*node, aOutArrayOfNodes, aIndex, aLists, aTables);
} else {
aOutArrayOfNodes.InsertElementAt(*aIndex, *node);
(*aIndex)++;
}
}
}
nsresult
HTMLEditRules::ExpandSelectionForDeletion()
{
MOZ_ASSERT(IsEditorDataAvailable());
// Don't need to touch collapsed selections
if (SelectionRef().IsCollapsed()) {
return NS_OK;
}
// We don't need to mess with cell selections, and we assume multirange
// selections are those.
if (SelectionRef().RangeCount() != 1) {
return NS_OK;
}
// Find current sel start and end
nsRange* firstRange = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsINode> selStartNode = firstRange->GetStartContainer();
int32_t selStartOffset = firstRange->StartOffset();
nsCOMPtr<nsINode> selEndNode = firstRange->GetEndContainer();
int32_t selEndOffset = firstRange->EndOffset();
// Find current selection common block parent
RefPtr<Element> selCommon =
HTMLEditor::GetBlock(*firstRange->GetCommonAncestor());
if (NS_WARN_IF(!selCommon)) {
return NS_ERROR_FAILURE;
}
// Set up for loops and cache our root element
nsCOMPtr<nsINode> firstBRParent;
nsCOMPtr<nsINode> unused;
int32_t visOffset = 0, firstBROffset = 0;
WSType wsType;
RefPtr<Element> root = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!root)) {
return NS_ERROR_FAILURE;
}
// Find previous visible things before start of selection
if (selStartNode != selCommon && selStartNode != root) {
while (true) {
WSRunObject wsObj(&HTMLEditorRef(), selStartNode, selStartOffset);
wsObj.PriorVisibleNode(EditorRawDOMPoint(selStartNode, selStartOffset),
address_of(unused), &visOffset, &wsType);
if (wsType != WSType::thisBlock) {
break;
}
// We want to keep looking up. But stop if we are crossing table
// element boundaries, or if we hit the root.
if (HTMLEditUtils::IsTableElement(wsObj.mStartReasonNode) ||
selCommon == wsObj.mStartReasonNode ||
root == wsObj.mStartReasonNode) {
break;
}
selStartNode = wsObj.mStartReasonNode->GetParentNode();
selStartOffset = selStartNode ?
selStartNode->ComputeIndexOf(wsObj.mStartReasonNode) : -1;
}
}
// Find next visible things after end of selection
if (selEndNode != selCommon && selEndNode != root) {
for (;;) {
WSRunObject wsObj(&HTMLEditorRef(), selEndNode, selEndOffset);
wsObj.NextVisibleNode(EditorRawDOMPoint(selEndNode, selEndOffset),
address_of(unused), &visOffset, &wsType);
if (wsType == WSType::br) {
if (HTMLEditorRef().IsVisibleBRElement(wsObj.mEndReasonNode)) {
break;
}
if (!firstBRParent) {
firstBRParent = selEndNode;
firstBROffset = selEndOffset;
}
selEndNode = wsObj.mEndReasonNode->GetParentNode();
selEndOffset = selEndNode
? selEndNode->ComputeIndexOf(wsObj.mEndReasonNode) + 1 : 0;
} else if (wsType == WSType::thisBlock) {
// We want to keep looking up. But stop if we are crossing table
// element boundaries, or if we hit the root.
if (HTMLEditUtils::IsTableElement(wsObj.mEndReasonNode) ||
selCommon == wsObj.mEndReasonNode ||
root == wsObj.mEndReasonNode) {
break;
}
selEndNode = wsObj.mEndReasonNode->GetParentNode();
selEndOffset = 1 + selEndNode->ComputeIndexOf(wsObj.mEndReasonNode);
} else {
break;
}
}
}
// Now set the selection to the new range
DebugOnly<nsresult> rv =
SelectionRef().Collapse(selStartNode, selStartOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to collapse selection");
// Expand selection endpoint only if we didn't pass a <br>, or if we really
// needed to pass that <br> (i.e., its block is now totally selected).
bool doEndExpansion = true;
if (firstBRParent) {
// Find block node containing <br>.
nsCOMPtr<Element> brBlock = HTMLEditor::GetBlock(*firstBRParent);
bool nodeBefore = false, nodeAfter = false;
// Create a range that represents expanded selection
RefPtr<nsRange> range = new nsRange(selStartNode);
nsresult rv = range->SetStartAndEnd(selStartNode, selStartOffset,
selEndNode, selEndOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Check if block is entirely inside range
if (brBlock) {
nsRange::CompareNodeToRange(brBlock, range, &nodeBefore, &nodeAfter);
}
// If block isn't contained, forgo grabbing the <br> in expanded selection.
if (nodeBefore || nodeAfter) {
doEndExpansion = false;
}
}
if (doEndExpansion) {
nsresult rv = SelectionRef().Extend(selEndNode, selEndOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
// Only expand to just before <br>.
nsresult rv = SelectionRef().Extend(firstBRParent, firstBROffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::NormalizeSelection()
{
MOZ_ASSERT(IsEditorDataAvailable());
// NormalizeSelection() tweaks non-collapsed selections to be more "natural".
// Idea here is to adjust selection endpoint so that they do not cross breaks
// or block boundaries unless something editable beyond that boundary is also
// selected. This adjustment makes it much easier for the various block
// operations to determine what nodes to act on.
// don't need to touch collapsed selections
if (SelectionRef().IsCollapsed()) {
return NS_OK;
}
// We don't need to mess with cell selections, and we assume multirange
// selections are those.
// XXX Why? Even in <input>, user can select 2 or more ranges.
if (SelectionRef().RangeCount() != 1) {
return NS_OK;
}
RefPtr<nsRange> range = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!range)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsINode> startNode = range->GetStartContainer();
if (NS_WARN_IF(!startNode)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsINode> endNode = range->GetEndContainer();
if (NS_WARN_IF(!endNode)) {
return NS_ERROR_FAILURE;
}
nsIContent* startChild = range->GetChildAtStartOffset();
nsIContent* endChild = range->GetChildAtEndOffset();
uint32_t startOffset = range->StartOffset();
uint32_t endOffset = range->EndOffset();
// adjusted values default to original values
nsCOMPtr<nsINode> newStartNode = startNode;
uint32_t newStartOffset = startOffset;
nsCOMPtr<nsINode> newEndNode = endNode;
uint32_t newEndOffset = endOffset;
// some locals we need for whitespace code
nsCOMPtr<nsINode> unused;
int32_t offset = -1;
WSType wsType;
// let the whitespace code do the heavy lifting
WSRunObject wsEndObj(&HTMLEditorRef(), endNode,
static_cast<int32_t>(endOffset));
// Is there any intervening visible whitespace? If so we can't push
// selection past that, it would visibly change meaning of users selection.
wsEndObj.PriorVisibleNode(EditorRawDOMPoint(endNode, endOffset),
address_of(unused), &offset, &wsType);
if (wsType != WSType::text && wsType != WSType::normalWS) {
// eThisBlock and eOtherBlock conveniently distinguish cases
// of going "down" into a block and "up" out of a block.
if (wsEndObj.mStartReason == WSType::otherBlock) {
// endpoint is just after the close of a block.
nsINode* child =
HTMLEditorRef().GetRightmostChild(wsEndObj.mStartReasonNode, true);
if (child) {
int32_t offset = -1;
newEndNode = EditorBase::GetNodeLocation(child, &offset);
// offset *after* child
newEndOffset = static_cast<uint32_t>(offset + 1);
}
// else block is empty - we can leave selection alone here, i think.
} else if (wsEndObj.mStartReason == WSType::thisBlock) {
// endpoint is just after start of this block
EditorRawDOMPoint atEnd(endNode, endChild, endOffset);
nsINode* child = HTMLEditorRef().GetPreviousEditableHTMLNode(atEnd);
if (child) {
int32_t offset = -1;
newEndNode = EditorBase::GetNodeLocation(child, &offset);
// offset *after* child
newEndOffset = static_cast<uint32_t>(offset + 1);
}
// else block is empty - we can leave selection alone here, i think.
} else if (wsEndObj.mStartReason == WSType::br) {
// endpoint is just after break. lets adjust it to before it.
int32_t offset = -1;
newEndNode =
EditorBase::GetNodeLocation(wsEndObj.mStartReasonNode, &offset);
newEndOffset = static_cast<uint32_t>(offset);;
}
}
// similar dealio for start of range
WSRunObject wsStartObj(&HTMLEditorRef(), startNode,
static_cast<int32_t>(startOffset));
// Is there any intervening visible whitespace? If so we can't push
// selection past that, it would visibly change meaning of users selection.
wsStartObj.NextVisibleNode(EditorRawDOMPoint(startNode, startOffset),
address_of(unused), &offset, &wsType);
if (wsType != WSType::text && wsType != WSType::normalWS) {
// eThisBlock and eOtherBlock conveniently distinguish cases
// of going "down" into a block and "up" out of a block.
if (wsStartObj.mEndReason == WSType::otherBlock) {
// startpoint is just before the start of a block.
nsINode* child =
HTMLEditorRef().GetLeftmostChild(wsStartObj.mEndReasonNode, true);
if (child) {
int32_t offset = -1;
newStartNode = EditorBase::GetNodeLocation(child, &offset);
newStartOffset = static_cast<uint32_t>(offset);
}
// else block is empty - we can leave selection alone here, i think.
} else if (wsStartObj.mEndReason == WSType::thisBlock) {
// startpoint is just before end of this block
nsINode* child =
HTMLEditorRef().GetNextEditableHTMLNode(
EditorRawDOMPoint(startNode,
startChild, startOffset));
if (child) {
int32_t offset = -1;
newStartNode = EditorBase::GetNodeLocation(child, &offset);
newStartOffset = static_cast<uint32_t>(offset);
}
// else block is empty - we can leave selection alone here, i think.
} else if (wsStartObj.mEndReason == WSType::br) {
// startpoint is just before a break. lets adjust it to after it.
int32_t offset = -1;
newStartNode =
EditorBase::GetNodeLocation(wsStartObj.mEndReasonNode, &offset);
// offset *after* break
newStartOffset = static_cast<uint32_t>(offset + 1);
}
}
// There is a demented possiblity we have to check for. We might have a very
// strange selection that is not collapsed and yet does not contain any
// editable content, and satisfies some of the above conditions that cause
// tweaking. In this case we don't want to tweak the selection into a block
// it was never in, etc. There are a variety of strategies one might use to
// try to detect these cases, but I think the most straightforward is to see
// if the adjusted locations "cross" the old values: i.e., new end before old
// start, or new start after old end. If so then just leave things alone.
int16_t comp;
comp = nsContentUtils::ComparePoints(startNode, startOffset,
newEndNode, newEndOffset);
if (comp == 1) {
return NS_OK; // New end before old start.
}
comp = nsContentUtils::ComparePoints(newStartNode, newStartOffset,
endNode, endOffset);
if (comp == 1) {
return NS_OK; // New start after old end.
}
// otherwise set selection to new values.
// XXX Why don't we use SetBaseAndExtent()?
DebugOnly<nsresult> rv =
SelectionRef().Collapse(newStartNode, newStartOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to collapse selection");
rv = SelectionRef().Extend(newEndNode, newEndOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to extend selection");
return NS_OK;
}
EditorDOMPoint
HTMLEditRules::GetPromotedPoint(RulesEndpoint aWhere,
nsINode& aNode,
int32_t aOffset,
EditSubAction aEditSubAction)
{
MOZ_ASSERT(IsEditorDataAvailable());
// we do one thing for text actions, something else entirely for other
// actions
if (aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME ||
aEditSubAction == EditSubAction::eInsertParagraphSeparator ||
aEditSubAction == EditSubAction::eDeleteText) {
bool isSpace, isNBSP;
nsCOMPtr<nsIContent> content =
aNode.IsContent() ? aNode.AsContent() : nullptr;
nsCOMPtr<nsIContent> temp;
int32_t newOffset = aOffset;
// for text actions, we want to look backwards (or forwards, as
// appropriate) for additional whitespace or nbsp's. We may have to act on
// these later even though they are outside of the initial selection. Even
// if they are in another node!
while (content) {
int32_t offset;
if (aWhere == kStart) {
HTMLEditorRef().IsPrevCharInNodeWhitespace(content, newOffset,
&isSpace, &isNBSP,
getter_AddRefs(temp),
&offset);
} else {
HTMLEditorRef().IsNextCharInNodeWhitespace(content, newOffset,
&isSpace, &isNBSP,
getter_AddRefs(temp),
&offset);
}
if (isSpace || isNBSP) {
content = temp;
newOffset = offset;
} else {
break;
}
}
return EditorDOMPoint(content, newOffset);
}
EditorDOMPoint point(&aNode, aOffset);
// else not a text section. In this case we want to see if we should grab
// any adjacent inline nodes and/or parents and other ancestors
if (aWhere == kStart) {
// some special casing for text nodes
if (point.IsInTextNode()) {
if (!point.GetContainer()->GetParentNode()) {
// Okay, can't promote any further
return point;
}
point.Set(point.GetContainer());
}
// look back through any further inline nodes that aren't across a <br>
// from us, and that are enclosed in the same block.
nsCOMPtr<nsINode> priorNode =
HTMLEditorRef().GetPreviousEditableHTMLNodeInBlock(point);
while (priorNode && priorNode->GetParentNode() &&
!HTMLEditorRef().IsVisibleBRElement(priorNode) &&
!IsBlockNode(*priorNode)) {
point.Set(priorNode);
priorNode = HTMLEditorRef().GetPreviousEditableHTMLNodeInBlock(point);
}
// finding the real start for this point. look up the tree for as long as
// we are the first node in the container, and as long as we haven't hit
// the body node.
nsCOMPtr<nsIContent> nearNode =
HTMLEditorRef().GetPreviousEditableHTMLNodeInBlock(point);
while (!nearNode &&
!point.IsContainerHTMLElement(nsGkAtoms::body) &&
point.GetContainer()->GetParentNode()) {
// some cutoffs are here: we don't need to also include them in the
// aWhere == kEnd case. as long as they are in one or the other it will
// work. special case for outdent: don't keep looking up if we have
// found a blockquote element to act on
if (aEditSubAction == EditSubAction::eOutdent &&
point.IsContainerHTMLElement(nsGkAtoms::blockquote)) {
break;
}
// Don't walk past the editable section. Note that we need to check
// before walking up to a parent because we need to return the parent
// object, so the parent itself might not be in the editable area, but
// it's OK if we're not performing a block-level action.
bool blockLevelAction =
aEditSubAction == EditSubAction::eIndent ||
aEditSubAction == EditSubAction::eOutdent ||
aEditSubAction == EditSubAction::eSetOrClearAlignment ||
aEditSubAction == EditSubAction::eCreateOrRemoveBlock;
if (!HTMLEditorRef().IsDescendantOfEditorRoot(
point.GetContainer()->GetParentNode()) &&
(blockLevelAction ||
!HTMLEditorRef().IsDescendantOfEditorRoot(point.GetContainer()))) {
break;
}
point.Set(point.GetContainer());
nearNode = HTMLEditorRef().GetPreviousEditableHTMLNodeInBlock(point);
}
return point;
}
// aWhere == kEnd
// some special casing for text nodes
if (point.IsInTextNode()) {
if (!point.GetContainer()->GetParentNode()) {
// Okay, can't promote any further
return point;
}
// want to be after the text node
point.Set(point.GetContainer());
DebugOnly<bool> advanced = point.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to after the text node");
}
// look ahead through any further inline nodes that aren't across a <br> from
// us, and that are enclosed in the same block.
// XXX Currently, we stop block-extending when finding visible <br> element.
// This might be different from "block-extend" of execCommand spec.
// However, the spec is really unclear.
// XXX Probably, scanning only editable nodes is wrong for
// EditSubAction::eCreateOrRemoveBlock because it might be better to wrap
// existing inline elements even if it's non-editable. For example,
// following examples with insertParagraph causes different result:
// * <div contenteditable>foo[]<b contenteditable="false">bar</b></div>
// * <div contenteditable>foo[]<b>bar</b></div>
// * <div contenteditable>foo[]<b contenteditable="false">bar</b>baz</div>
// Only in the first case, after the caret position isn't wrapped with
// new <div> element.
nsCOMPtr<nsIContent> nextNode =
HTMLEditorRef().GetNextEditableHTMLNodeInBlock(point);
while (nextNode && !IsBlockNode(*nextNode) && nextNode->GetParentNode()) {
point.Set(nextNode);
if (NS_WARN_IF(!point.AdvanceOffset())) {
break;
}
if (HTMLEditorRef().IsVisibleBRElement(nextNode)) {
break;
}
// Check for newlines in pre-formatted text nodes.
if (EditorBase::IsPreformatted(nextNode) &&
EditorBase::IsTextNode(nextNode)) {
nsAutoString tempString;
nextNode->GetAsText()->GetData(tempString);
int32_t newlinePos = tempString.FindChar(nsCRT::LF);
if (newlinePos >= 0) {
if (static_cast<uint32_t>(newlinePos) + 1 == tempString.Length()) {
// No need for special processing if the newline is at the end.
break;
}
return EditorDOMPoint(nextNode, newlinePos + 1);
}
}
nextNode = HTMLEditorRef().GetNextEditableHTMLNodeInBlock(point);
}
// finding the real end for this point. look up the tree for as long as we
// are the last node in the container, and as long as we haven't hit the body
// node.
nsCOMPtr<nsIContent> nearNode =
HTMLEditorRef().GetNextEditableHTMLNodeInBlock(point);
while (!nearNode &&
!point.IsContainerHTMLElement(nsGkAtoms::body) &&
point.GetContainer()->GetParentNode()) {
// Don't walk past the editable section. Note that we need to check before
// walking up to a parent because we need to return the parent object, so
// the parent itself might not be in the editable area, but it's OK.
if (!HTMLEditorRef().IsDescendantOfEditorRoot(point.GetContainer()) &&
!HTMLEditorRef().IsDescendantOfEditorRoot(
point.GetContainer()->GetParentNode())) {
break;
}
point.Set(point.GetContainer());
if (NS_WARN_IF(!point.AdvanceOffset())) {
break;
}
nearNode = HTMLEditorRef().GetNextEditableHTMLNodeInBlock(point);
}
return point;
}
void
HTMLEditRules::GetPromotedRanges(nsTArray<RefPtr<nsRange>>& outArrayOfRanges,
EditSubAction aEditSubAction)
{
MOZ_ASSERT(IsEditorDataAvailable());
uint32_t rangeCount = SelectionRef().RangeCount();
for (uint32_t i = 0; i < rangeCount; i++) {
RefPtr<nsRange> selectionRange = SelectionRef().GetRangeAt(i);
MOZ_ASSERT(selectionRange);
// Clone range so we don't muck with actual selection ranges
RefPtr<nsRange> opRange = selectionRange->CloneRange();
// Make a new adjusted range to represent the appropriate block content.
// The basic idea is to push out the range endpoints to truly enclose the
// blocks that we will affect. This call alters opRange.
PromoteRange(*opRange, aEditSubAction);
// Stuff new opRange into array
outArrayOfRanges.AppendElement(opRange);
}
}
void
HTMLEditRules::PromoteRange(nsRange& aRange,
EditSubAction aEditSubAction)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(!aRange.IsInSelection());
if (!aRange.IsPositioned()) {
return;
}
nsCOMPtr<nsINode> startNode = aRange.GetStartContainer();
nsCOMPtr<nsINode> endNode = aRange.GetEndContainer();
int32_t startOffset = aRange.StartOffset();
int32_t endOffset = aRange.EndOffset();
// MOOSE major hack:
// GetPromotedPoint doesn't really do the right thing for collapsed ranges
// inside block elements that contain nothing but a solo <br>. It's easier
// to put a workaround here than to revamp GetPromotedPoint. :-(
if (startNode == endNode && startOffset == endOffset) {
RefPtr<Element> block = HTMLEditorRef().GetBlock(*startNode);
if (block) {
bool bIsEmptyNode = false;
nsIContent* host = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!host)) {
return;
}
// Make sure we don't go higher than our root element in the content tree
if (!nsContentUtils::ContentIsDescendantOf(host, block)) {
HTMLEditorRef().IsEmptyNode(block, &bIsEmptyNode, true, false);
}
if (bIsEmptyNode) {
startNode = block;
endNode = block;
startOffset = 0;
endOffset = block->Length();
}
}
}
if (aEditSubAction == EditSubAction::eInsertText ||
aEditSubAction == EditSubAction::eInsertTextComingFromIME ||
aEditSubAction == EditSubAction::eInsertParagraphSeparator ||
aEditSubAction == EditSubAction::eDeleteText) {
if (!startNode->IsContent() ||
!endNode->IsContent()) {
// GetPromotedPoint cannot promote node when action type is text
// operation and selected node isn't content node.
return;
}
}
// Make a new adjusted range to represent the appropriate block content.
// This is tricky. The basic idea is to push out the range endpoints to
// truly enclose the blocks that we will affect.
// Make sure that the new range ends up to be in the editable section.
// XXX Looks like that this check wastes the time. Perhaps, we should
// implement a method which checks both two DOM points in the editor
// root.
EditorDOMPoint startPoint =
GetPromotedPoint(kStart, *startNode, startOffset, aEditSubAction);
if (!HTMLEditorRef().IsDescendantOfEditorRoot(
EditorBase::GetNodeAtRangeOffsetPoint(startPoint))) {
return;
}
EditorDOMPoint endPoint =
GetPromotedPoint(kEnd, *endNode, endOffset, aEditSubAction);
EditorRawDOMPoint lastRawPoint(endPoint);
lastRawPoint.RewindOffset();
if (!HTMLEditorRef().IsDescendantOfEditorRoot(
EditorBase::GetNodeAtRangeOffsetPoint(lastRawPoint))) {
return;
}
DebugOnly<nsresult> rv = aRange.SetStartAndEnd(startPoint, endPoint);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
class UniqueFunctor final : public BoolDomIterFunctor
{
public:
explicit UniqueFunctor(nsTArray<OwningNonNull<nsINode>>& aArray)
: mArray(aArray)
{
}
// Used to build list of all nodes iterator covers.
virtual bool operator()(nsINode* aNode) const override
{
return !mArray.Contains(aNode);
}
private:
nsTArray<OwningNonNull<nsINode>>& mArray;
};
nsresult
HTMLEditRules::GetNodesForOperation(
nsTArray<RefPtr<nsRange>>& aArrayOfRanges,
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
EditSubAction aEditSubAction,
TouchContent aTouchContent)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (aTouchContent == TouchContent::yes) {
// Split text nodes. This is necessary, since GetPromotedPoint() may return a
// range ending in a text node in case where part of a pre-formatted
// elements needs to be moved.
for (RefPtr<nsRange>& range : aArrayOfRanges) {
EditorDOMPoint atEnd(range->EndRef());
if (NS_WARN_IF(!atEnd.IsSet()) || !atEnd.IsInTextNode()) {
continue;
}
if (!atEnd.IsStartOfContainer() && !atEnd.IsEndOfContainer()) {
// Split the text node.
ErrorResult error;
nsCOMPtr<nsIContent> newLeftNode =
HTMLEditorRef().SplitNodeWithTransaction(atEnd, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
// Correct the range.
// The new end parent becomes the parent node of the text.
EditorRawDOMPoint atContainerOfSplitNode(atEnd.GetContainer());
MOZ_ASSERT(!range->IsInSelection());
range->SetEnd(atContainerOfSplitNode, error);
if (NS_WARN_IF(error.Failed())) {
error.SuppressException();
}
}
}
}
// Bust up any inlines that cross our range endpoints, but only if we are
// allowed to touch content.
// XXX Why don't we merge this block with the previous block?
if (aTouchContent == TouchContent::yes) {
nsTArray<OwningNonNull<RangeItem>> rangeItemArray;
rangeItemArray.AppendElements(aArrayOfRanges.Length());
// First register ranges for special editor gravity
for (auto& rangeItem : rangeItemArray) {
rangeItem = new RangeItem();
rangeItem->StoreRange(aArrayOfRanges[0]);
HTMLEditorRef().mRangeUpdater.RegisterRangeItem(rangeItem);
aArrayOfRanges.RemoveElementAt(0);
}
// Now bust up inlines.
for (auto& item : Reversed(rangeItemArray)) {
nsresult rv = BustUpInlinesAtRangeEndpoints(*item);
if (NS_FAILED(rv)) {
break;
}
}
// Then unregister the ranges
for (auto& item : rangeItemArray) {
HTMLEditorRef().mRangeUpdater.DropRangeItem(item);
RefPtr<nsRange> range = item->GetRange();
if (range) {
aArrayOfRanges.AppendElement(range);
}
}
}
// Gather up a list of all the nodes
for (auto& range : aArrayOfRanges) {
DOMSubtreeIterator iter;
nsresult rv = iter.Init(*range);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (aOutArrayOfNodes.IsEmpty()) {
iter.AppendList(TrivialFunctor(), aOutArrayOfNodes);
} else {
// We don't want duplicates in aOutArrayOfNodes, so we use an
// iterator/functor that only return nodes that are not already in
// aOutArrayOfNodes.
nsTArray<OwningNonNull<nsINode>> nodes;
iter.AppendList(UniqueFunctor(aOutArrayOfNodes), nodes);
aOutArrayOfNodes.AppendElements(nodes);
}
}
// Certain operations should not act on li's and td's, but rather inside
// them. Alter the list as needed.
if (aEditSubAction == EditSubAction::eCreateOrRemoveBlock) {
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
if (HTMLEditUtils::IsListItem(node)) {
int32_t j = i;
aOutArrayOfNodes.RemoveElementAt(i);
GetInnerContent(*node, aOutArrayOfNodes, &j);
}
}
// Empty text node shouldn't be selected if unnecessary
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
if (Text* text = aOutArrayOfNodes[i]->GetAsText()) {
// Don't select empty text except to empty block
if (!HTMLEditorRef().IsVisibleTextNode(*text)) {
aOutArrayOfNodes.RemoveElementAt(i);
}
}
}
}
// Indent/outdent already do something special for list items, but we still
// need to make sure we don't act on table elements
else if (aEditSubAction == EditSubAction::eOutdent ||
aEditSubAction == EditSubAction::eIndent ||
aEditSubAction == EditSubAction::eSetPositionToAbsolute) {
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
if (HTMLEditUtils::IsTableElementButNotTable(node)) {
int32_t j = i;
aOutArrayOfNodes.RemoveElementAt(i);
GetInnerContent(*node, aOutArrayOfNodes, &j);
}
}
}
// Outdent should look inside of divs.
if (aEditSubAction == EditSubAction::eOutdent &&
!HTMLEditorRef().IsCSSEnabled()) {
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
if (node->IsHTMLElement(nsGkAtoms::div)) {
int32_t j = i;
aOutArrayOfNodes.RemoveElementAt(i);
GetInnerContent(*node, aOutArrayOfNodes, &j, Lists::no, Tables::no);
}
}
}
// Post-process the list to break up inline containers that contain br's, but
// only for operations that might care, like making lists or paragraphs
if (aEditSubAction == EditSubAction::eCreateOrRemoveBlock ||
aEditSubAction == EditSubAction::eCreateOrChangeList ||
aEditSubAction == EditSubAction::eSetOrClearAlignment ||
aEditSubAction == EditSubAction::eSetPositionToAbsolute ||
aEditSubAction == EditSubAction::eIndent ||
aEditSubAction == EditSubAction::eOutdent) {
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
// XXX Why do we run this loop even when aTouchContent is "no"?
if (aTouchContent == TouchContent::yes && IsInlineNode(node) &&
HTMLEditorRef().IsContainer(node) && !EditorBase::IsTextNode(node)) {
nsTArray<OwningNonNull<nsINode>> arrayOfInlines;
nsresult rv = BustUpInlinesAtBRs(*node->AsContent(), arrayOfInlines);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Put these nodes in aOutArrayOfNodes, replacing the current node
aOutArrayOfNodes.RemoveElementAt(i);
aOutArrayOfNodes.InsertElementsAt(i, arrayOfInlines);
}
}
}
return NS_OK;
}
void
HTMLEditRules::GetChildNodesForOperation(
nsINode& aNode,
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes)
{
for (nsCOMPtr<nsIContent> child = aNode.GetFirstChild();
child; child = child->GetNextSibling()) {
outArrayOfNodes.AppendElement(*child);
}
}
nsresult
HTMLEditRules::GetListActionNodes(
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
EntireList aEntireList,
TouchContent aTouchContent)
{
MOZ_ASSERT(IsEditorDataAvailable());
// Added this in so that ui code can ask to change an entire list, even if
// selection is only in part of it. used by list item dialog.
if (aEntireList == EntireList::yes) {
uint32_t rangeCount = SelectionRef().RangeCount();
for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
RefPtr<nsRange> range = SelectionRef().GetRangeAt(rangeIdx);
for (nsCOMPtr<nsINode> parent = range->GetCommonAncestor();
parent; parent = parent->GetParentNode()) {
if (HTMLEditUtils::IsList(parent)) {
aOutArrayOfNodes.AppendElement(*parent);
break;
}
}
}
// If we didn't find any nodes this way, then try the normal way. Perhaps
// the selection spans multiple lists but with no common list parent.
if (!aOutArrayOfNodes.IsEmpty()) {
return NS_OK;
}
}
{
// We don't like other people messing with our selection!
AutoTransactionsConserveSelection dontChangeMySelection(&HTMLEditorRef());
// contruct a list of nodes to act on.
nsresult rv = GetNodesFromSelection(EditSubAction::eCreateOrChangeList,
aOutArrayOfNodes, aTouchContent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Pre-process our list of nodes
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> testNode = aOutArrayOfNodes[i];
// Remove all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(testNode)) {
aOutArrayOfNodes.RemoveElementAt(i);
continue;
}
// Scan for table elements and divs. If we find table elements other than
// table, replace it with a list of any editable non-table content.
if (HTMLEditUtils::IsTableElementButNotTable(testNode)) {
int32_t j = i;
aOutArrayOfNodes.RemoveElementAt(i);
GetInnerContent(*testNode, aOutArrayOfNodes, &j, Lists::no);
}
}
// 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(aOutArrayOfNodes);
return NS_OK;
}
void
HTMLEditRules::LookInsideDivBQandList(
nsTArray<OwningNonNull<nsINode>>& aNodeArray)
{
MOZ_ASSERT(IsEditorDataAvailable());
// 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.
if (aNodeArray.Length() != 1) {
return;
}
OwningNonNull<nsINode> curNode = aNodeArray[0];
while (curNode->IsHTMLElement(nsGkAtoms::div) ||
HTMLEditUtils::IsList(curNode) ||
curNode->IsHTMLElement(nsGkAtoms::blockquote)) {
// Dive as long as there's only one child, and it's a list, div, blockquote
uint32_t numChildren = HTMLEditorRef().CountEditableChildren(curNode);
if (numChildren != 1) {
break;
}
// Keep diving! XXX One would expect to dive into the one editable node.
nsCOMPtr<nsIContent> child = curNode->GetFirstChild();
if (!child->IsHTMLElement(nsGkAtoms::div) &&
!HTMLEditUtils::IsList(child) &&
!child->IsHTMLElement(nsGkAtoms::blockquote)) {
break;
}
// check editability XXX floppy moose
curNode = child;
}
// We've found innermost list/blockquote/div: replace the one node in the
// array with these nodes
aNodeArray.RemoveElementAt(0);
if (curNode->IsAnyOfHTMLElements(nsGkAtoms::div,
nsGkAtoms::blockquote)) {
int32_t j = 0;
GetInnerContent(*curNode, aNodeArray, &j, Lists::no, Tables::no);
return;
}
aNodeArray.AppendElement(*curNode);
}
void
HTMLEditRules::GetDefinitionListItemTypes(dom::Element* aElement,
bool* aDT,
bool* aDD)
{
MOZ_ASSERT(aElement);
MOZ_ASSERT(aElement->IsHTMLElement(nsGkAtoms::dl));
MOZ_ASSERT(aDT);
MOZ_ASSERT(aDD);
*aDT = *aDD = false;
for (nsIContent* child = aElement->GetFirstChild();
child;
child = child->GetNextSibling()) {
if (child->IsHTMLElement(nsGkAtoms::dt)) {
*aDT = true;
} else if (child->IsHTMLElement(nsGkAtoms::dd)) {
*aDD = true;
}
}
}
nsresult
HTMLEditRules::GetParagraphFormatNodes(
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes)
{
MOZ_ASSERT(IsEditorDataAvailable());
// Contruct a list of nodes to act on.
nsresult rv =
GetNodesFromSelection(EditSubAction::eCreateOrRemoveBlock,
outArrayOfNodes, TouchContent::no);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Pre-process our list of nodes
for (int32_t i = outArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> testNode = outArrayOfNodes[i];
// Remove all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(testNode)) {
outArrayOfNodes.RemoveElementAt(i);
continue;
}
// Scan for table elements. If we find table elements other than table,
// replace it with a list of any editable non-table content. Ditto for
// list elements.
if (HTMLEditUtils::IsTableElement(testNode) ||
HTMLEditUtils::IsList(testNode) ||
HTMLEditUtils::IsListItem(testNode)) {
int32_t j = i;
outArrayOfNodes.RemoveElementAt(i);
GetInnerContent(testNode, outArrayOfNodes, &j);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::BustUpInlinesAtRangeEndpoints(RangeItem& aRangeItem)
{
MOZ_ASSERT(IsEditorDataAvailable());
bool isCollapsed = aRangeItem.mStartContainer == aRangeItem.mEndContainer &&
aRangeItem.mStartOffset == aRangeItem.mEndOffset;
nsCOMPtr<nsIContent> endInline =
GetHighestInlineParent(*aRangeItem.mEndContainer);
// XXX Oh, then, if the range is collapsed, we don't need to call
// GetHighestInlineParent(), isn't it?
if (endInline && !isCollapsed) {
SplitNodeResult splitEndInlineResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*endInline,
EditorRawDOMPoint(aRangeItem.mEndContainer,
aRangeItem.mEndOffset),
SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitEndInlineResult.Failed())) {
return splitEndInlineResult.Rv();
}
EditorRawDOMPoint splitPointAtEnd(splitEndInlineResult.SplitPoint());
if (NS_WARN_IF(!splitPointAtEnd.IsSet())) {
return NS_ERROR_FAILURE;
}
aRangeItem.mEndContainer = splitPointAtEnd.GetContainer();
aRangeItem.mEndOffset = splitPointAtEnd.Offset();
}
nsCOMPtr<nsIContent> startInline =
GetHighestInlineParent(*aRangeItem.mStartContainer);
if (startInline) {
SplitNodeResult splitStartInlineResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*startInline,
EditorRawDOMPoint(aRangeItem.mStartContainer,
aRangeItem.mStartOffset),
SplitAtEdges::eDoNotCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitStartInlineResult.Failed())) {
return splitStartInlineResult.Rv();
}
// XXX If we split only here because of collapsed range, we're modifying
// only start point of aRangeItem. Shouldn't we modify end point here
// if it's collapsed?
EditorRawDOMPoint splitPointAtStart(splitStartInlineResult.SplitPoint());
if (NS_WARN_IF(!splitPointAtStart.IsSet())) {
return NS_ERROR_FAILURE;
}
aRangeItem.mStartContainer = splitPointAtStart.GetContainer();
aRangeItem.mStartOffset = splitPointAtStart.Offset();
}
return NS_OK;
}
nsresult
HTMLEditRules::BustUpInlinesAtBRs(
nsIContent& aNode,
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes)
{
MOZ_ASSERT(IsEditorDataAvailable());
// First build up a list of all the break nodes inside the inline container.
nsTArray<OwningNonNull<nsINode>> arrayOfBreaks;
BRNodeFunctor functor;
DOMIterator iter(aNode);
iter.AppendList(functor, arrayOfBreaks);
// If there aren't any breaks, just put inNode itself in the array
if (arrayOfBreaks.IsEmpty()) {
aOutArrayOfNodes.AppendElement(aNode);
return NS_OK;
}
// Else we need to bust up aNode along all the breaks
nsCOMPtr<nsIContent> nextNode = &aNode;
for (OwningNonNull<nsINode>& brNode : arrayOfBreaks) {
EditorRawDOMPoint atBrNode(brNode);
if (NS_WARN_IF(!atBrNode.IsSet())) {
return NS_ERROR_FAILURE;
}
SplitNodeResult splitNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*nextNode, atBrNode,
SplitAtEdges::eAllowToCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
// Put previous node at the split point.
if (splitNodeResult.GetPreviousNode()) {
// Might not be a left node. A break might have been at the very
// beginning of inline container, in which case
// SplitNodeDeepWithTransaction() would not actually split anything.
aOutArrayOfNodes.AppendElement(*splitNodeResult.GetPreviousNode());
}
// Move break outside of container and also put in node list
EditorRawDOMPoint atNextNode(splitNodeResult.GetNextNode());
nsresult rv =
HTMLEditorRef().MoveNodeWithTransaction(*brNode->AsContent(), atNextNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
aOutArrayOfNodes.AppendElement(*brNode);
nextNode = splitNodeResult.GetNextNode();
}
// Now tack on remaining next node.
aOutArrayOfNodes.AppendElement(*nextNode);
return NS_OK;
}
nsIContent*
HTMLEditRules::GetHighestInlineParent(nsINode& aNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (!aNode.IsContent() || IsBlockNode(aNode)) {
return nullptr;
}
Element* host = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!host)) {
return nullptr;
}
// If aNode is the editing host itself, there is no modifiable inline parent.
if (&aNode == host) {
return nullptr;
}
// If aNode is outside of the <body> element, we don't support to edit
// such elements for now.
// XXX This should be MOZ_ASSERT after fixing bug 1413131 for avoiding
// calling this expensive method.
if (NS_WARN_IF(!EditorUtils::IsDescendantOf(aNode, *host))) {
return nullptr;
}
// Looks for the highest inline parent in the editing host.
nsIContent* content = aNode.AsContent();
for (nsIContent* parent = content->GetParent();
parent && parent != host && IsInlineNode(*parent);
parent = parent->GetParent()) {
content = parent;
}
return content;
}
nsresult
HTMLEditRules::GetNodesFromPoint(
const EditorDOMPoint& aPoint,
EditSubAction aEditSubAction,
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
TouchContent aTouchContent)
{
if (NS_WARN_IF(!aPoint.IsSet())) {
return NS_ERROR_INVALID_ARG;
}
RefPtr<nsRange> range = new nsRange(aPoint.GetContainer());
IgnoredErrorResult ignoredError;
range->SetStart(aPoint, ignoredError);
// error will assert on failure, because we are not cleaning it up,
// but we're asserting in that case anyway.
MOZ_ASSERT(!ignoredError.Failed());
// Expand the range to include adjacent inlines
PromoteRange(*range, aEditSubAction);
// Make array of ranges
nsTArray<RefPtr<nsRange>> arrayOfRanges;
// Stuff new opRange into array
arrayOfRanges.AppendElement(range);
// Use these ranges to contruct a list of nodes to act on
nsresult rv =
GetNodesForOperation(arrayOfRanges, outArrayOfNodes, aEditSubAction,
aTouchContent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::GetNodesFromSelection(
EditSubAction aEditSubAction,
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
TouchContent aTouchContent)
{
MOZ_ASSERT(IsEditorDataAvailable());
// Promote selection ranges
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(arrayOfRanges, aEditSubAction);
// Use these ranges to contruct a list of nodes to act on.
nsresult rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes,
aEditSubAction, aTouchContent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
void
HTMLEditRules::MakeTransitionList(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
nsTArray<bool>& aTransitionArray)
{
nsCOMPtr<nsINode> prevParent;
aTransitionArray.EnsureLengthAtLeast(aNodeArray.Length());
for (uint32_t i = 0; i < aNodeArray.Length(); i++) {
if (aNodeArray[i]->GetParentNode() != prevParent) {
// Different parents: transition point
aTransitionArray[i] = true;
} else {
// Same parents: these nodes grew up together
aTransitionArray[i] = false;
}
prevParent = aNodeArray[i]->GetParentNode();
}
}
Element*
HTMLEditRules::IsInListItem(nsINode* aNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
NS_ENSURE_TRUE(aNode, nullptr);
if (HTMLEditUtils::IsListItem(aNode)) {
return aNode->AsElement();
}
Element* parent = aNode->GetParentElement();
while (parent &&
HTMLEditorRef().IsDescendantOfEditorRoot(parent) &&
!HTMLEditUtils::IsTableElement(parent)) {
if (HTMLEditUtils::IsListItem(parent)) {
return parent;
}
parent = parent->GetParentElement();
}
return nullptr;
}
nsAtom&
HTMLEditRules::DefaultParagraphSeparator()
{
MOZ_ASSERT(IsEditorDataAvailable());
return ParagraphSeparatorElement(
HTMLEditorRef().GetDefaultParagraphSeparator());
}
nsresult
HTMLEditRules::ReturnInHeader(Element& aHeader,
nsINode& aNode,
int32_t aOffset)
{
MOZ_ASSERT(IsEditorDataAvailable());
// Remember where the header is
nsCOMPtr<nsINode> headerParent = aHeader.GetParentNode();
int32_t offset = headerParent ? headerParent->ComputeIndexOf(&aHeader) : -1;
// Get ws code to adjust any ws
nsCOMPtr<nsINode> node = &aNode;
nsresult rv = WSRunObject::PrepareToSplitAcrossBlocks(&HTMLEditorRef(),
address_of(node),
&aOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (NS_WARN_IF(!node->IsContent())) {
return NS_ERROR_FAILURE;
}
// Split the header
SplitNodeResult splitHeaderResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
aHeader, EditorRawDOMPoint(node, aOffset),
SplitAtEdges::eAllowToCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(splitHeaderResult.Succeeded(),
"Failed to split aHeader");
// If the previous heading of split point is empty, put a mozbr into it.
nsCOMPtr<nsIContent> prevItem = HTMLEditorRef().GetPriorHTMLSibling(&aHeader);
if (prevItem) {
MOZ_DIAGNOSTIC_ASSERT(
HTMLEditUtils::IsHeader(*prevItem));
bool isEmptyNode;
rv = HTMLEditorRef().IsEmptyNode(prevItem, &isEmptyNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (isEmptyNode) {
CreateElementResult createMozBrResult =
CreateMozBR(EditorRawDOMPoint(prevItem, 0));
if (NS_WARN_IF(createMozBrResult.Failed())) {
return createMozBrResult.Rv();
}
}
}
// If the new (righthand) header node is empty, delete it
if (IsEmptyBlockElement(aHeader, IgnoreSingleBR::eYes)) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(aHeader);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Layout tells the caret to blink in a weird place if we don't place a
// break after the header.
nsCOMPtr<nsIContent> sibling;
if (aHeader.GetNextSibling()) {
sibling = HTMLEditorRef().GetNextHTMLSibling(aHeader.GetNextSibling());
}
if (!sibling || !sibling->IsHTMLElement(nsGkAtoms::br)) {
ClearCachedStyles();
HTMLEditorRef().mTypeInState->ClearAllProps();
// Create a paragraph
nsAtom& paraAtom = DefaultParagraphSeparator();
// We want a wrapper element even if we separate with <br>
EditorRawDOMPoint nextToHeader(headerParent, offset + 1);
RefPtr<Element> pNode =
HTMLEditorRef().CreateNodeWithTransaction(&paraAtom == nsGkAtoms::br ?
*nsGkAtoms::p : paraAtom,
nextToHeader);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!pNode)) {
return NS_ERROR_FAILURE;
}
// Append a <br> to it
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(
SelectionRef(), EditorRawDOMPoint(pNode, 0));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
// Set selection to before the break
ErrorResult error;
SelectionRef().Collapse(EditorRawDOMPoint(pNode, 0), error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
} else {
EditorRawDOMPoint afterSibling(sibling);
if (NS_WARN_IF(!afterSibling.AdvanceOffset())) {
return NS_ERROR_FAILURE;
}
// Put selection after break
ErrorResult error;
SelectionRef().Collapse(afterSibling, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
} else {
// Put selection at front of righthand heading
ErrorResult error;
SelectionRef().Collapse(RawRangeBoundary(&aHeader, 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;
}
EditActionResult
HTMLEditRules::ReturnInParagraph(Element& aParentDivOrP)
{
MOZ_ASSERT(IsEditorDataAvailable());
nsRange* firstRange = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return EditActionResult(NS_ERROR_FAILURE);
}
EditorDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return EditActionResult(NS_ERROR_FAILURE);
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
// We shouldn't create new anchor element which has non-empty href unless
// splitting middle of it because we assume that users don't want to create
// *same* anchor element across two or more paragraphs in most cases.
// So, adjust selection start if it's edge of anchor element(s).
// XXX We don't support whitespace collapsing in these cases since it needs
// some additional work with WSRunObject but it's not usual case.
// E.g., |<a href="foo"><b>foo []</b> </a>|
if (atStartOfSelection.IsStartOfContainer()) {
for (nsIContent* container = atStartOfSelection.GetContainerAsContent();
container && container != &aParentDivOrP;
container = container->GetParent()) {
if (HTMLEditUtils::IsLink(container)) {
// Found link should be only in right node. So, we shouldn't split it.
atStartOfSelection.Set(container);
// Even if we found an anchor element, don't break because DOM API
// allows to nest anchor elements.
}
// If the container is middle of its parent, stop adjusting split point.
if (container->GetPreviousSibling()) {
// XXX Should we check if previous sibling is visible content?
// E.g., should we ignore comment node, invisible <br> element?
break;
}
}
}
// We also need to check if selection is at invisible <br> element at end
// of an <a href="foo"> element because editor inserts a <br> element when
// user types Enter key after a whitespace which is at middle of
// <a href="foo"> element and when setting selection at end of the element,
// selection becomes referring the <br> element. We may need to change this
// behavior later if it'd be standardized.
else if (atStartOfSelection.IsEndOfContainer() ||
atStartOfSelection.IsBRElementAtEndOfContainer()) {
// If there are 2 <br> elements, the first <br> element is visible. E.g.,
// |<a href="foo"><b>boo[]<br></b><br></a>|, we should split the <a>
// element. Otherwise, E.g., |<a href="foo"><b>boo[]<br></b></a>|,
// we should not split the <a> element and ignore inline elements in it.
bool foundBRElement = atStartOfSelection.IsBRElementAtEndOfContainer();
for (nsIContent* container = atStartOfSelection.GetContainerAsContent();
container && container != &aParentDivOrP;
container = container->GetParent()) {
if (HTMLEditUtils::IsLink(container)) {
// Found link should be only in left node. So, we shouldn't split it.
atStartOfSelection.SetAfter(container);
// Even if we found an anchor element, don't break because DOM API
// allows to nest anchor elements.
}
// If the container is middle of its parent, stop adjusting split point.
if (nsIContent* nextSibling = container->GetNextSibling()) {
if (foundBRElement) {
// If we've already found a <br> element, we assume found node is
// visible <br> or something other node.
// XXX Should we check if non-text data node like comment?
break;
}
// XXX Should we check if non-text data node like comment?
if (!nextSibling->IsHTMLElement(nsGkAtoms::br)) {
break;
}
foundBRElement = true;
}
}
}
bool doesCRCreateNewP =
HTMLEditorRef().GetReturnInParagraphCreatesNewParagraph();
bool splitAfterNewBR = false;
nsCOMPtr<nsIContent> brContent;
EditorDOMPoint pointToSplitParentDivOrP(atStartOfSelection);
EditorRawDOMPoint pointToInsertBR;
if (doesCRCreateNewP &&
atStartOfSelection.GetContainer() == &aParentDivOrP) {
// We are at the edges of the block, so, we don't need to create new <br>.
brContent = nullptr;
} else if (atStartOfSelection.IsInTextNode()) {
// at beginning of text node?
if (atStartOfSelection.IsStartOfContainer()) {
// is there a BR prior to it?
brContent =
HTMLEditorRef().GetPriorHTMLSibling(atStartOfSelection.GetContainer());
if (!brContent ||
!HTMLEditorRef().IsVisibleBRElement(brContent) ||
TextEditUtils::HasMozAttr(brContent)) {
pointToInsertBR.Set(atStartOfSelection.GetContainer());
brContent = nullptr;
}
} else if (atStartOfSelection.IsEndOfContainer()) {
// we're at the end of text node...
// is there a BR after to it?
brContent =
HTMLEditorRef().GetNextHTMLSibling(atStartOfSelection.GetContainer());
if (!brContent ||
!HTMLEditorRef().IsVisibleBRElement(brContent) ||
TextEditUtils::HasMozAttr(brContent)) {
pointToInsertBR.Set(atStartOfSelection.GetContainer());
DebugOnly<bool> advanced = pointToInsertBR.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to after the container of selection start");
brContent = nullptr;
}
} else {
if (doesCRCreateNewP) {
ErrorResult error;
nsCOMPtr<nsIContent> newLeftDivOrP =
HTMLEditorRef().SplitNodeWithTransaction(pointToSplitParentDivOrP,
error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
if (NS_WARN_IF(error.Failed())) {
return EditActionResult(error.StealNSResult());
}
pointToSplitParentDivOrP.SetToEndOf(newLeftDivOrP);
}
// We need to put new <br> after the left node if given node was split
// above.
pointToInsertBR.Set(pointToSplitParentDivOrP.GetContainer());
DebugOnly<bool> advanced = pointToInsertBR.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to after the container of selection start");
}
} else {
// not in a text node.
// is there a BR prior to it?
nsCOMPtr<nsIContent> nearNode;
nearNode = HTMLEditorRef().GetPreviousEditableHTMLNode(atStartOfSelection);
if (!nearNode || !HTMLEditorRef().IsVisibleBRElement(nearNode) ||
TextEditUtils::HasMozAttr(nearNode)) {
// is there a BR after it?
nearNode = HTMLEditorRef().GetNextEditableHTMLNode(atStartOfSelection);
if (!nearNode || !HTMLEditorRef().IsVisibleBRElement(nearNode) ||
TextEditUtils::HasMozAttr(nearNode)) {
pointToInsertBR = atStartOfSelection;
splitAfterNewBR = true;
}
}
if (!pointToInsertBR.IsSet() && TextEditUtils::IsBreak(nearNode)) {
brContent = nearNode;
}
}
if (pointToInsertBR.IsSet()) {
// if CR does not create a new P, default to BR creation
if (NS_WARN_IF(!doesCRCreateNewP)) {
return EditActionResult(NS_OK);
}
brContent =
HTMLEditorRef().InsertBrElementWithTransaction(SelectionRef(),
pointToInsertBR);
if (NS_WARN_IF(!CanHandleEditAction())) {
return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(brContent, "Failed to create a <br> element");
if (splitAfterNewBR) {
// We split the parent after the br we've just inserted.
pointToSplitParentDivOrP.Set(brContent);
DebugOnly<bool> advanced = pointToSplitParentDivOrP.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after the new <br>");
}
}
EditActionResult result(
SplitParagraph(aParentDivOrP, pointToSplitParentDivOrP, brContent));
result.MarkAsHandled();
if (NS_WARN_IF(result.Failed())) {
return result;
}
return result;
}
template<typename PT, typename CT>
nsresult
HTMLEditRules::SplitParagraph(
Element& aParentDivOrP,
const EditorDOMPointBase<PT, CT>& aStartOfRightNode,
nsIContent* aNextBRNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
// split para
// get ws code to adjust any ws
nsCOMPtr<nsINode> selNode = aStartOfRightNode.GetContainer();
int32_t selOffset = aStartOfRightNode.Offset();
nsresult rv =
WSRunObject::PrepareToSplitAcrossBlocks(&HTMLEditorRef(),
address_of(selNode), &selOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (NS_WARN_IF(!selNode->IsContent())) {
return NS_ERROR_FAILURE;
}
// Split the paragraph.
SplitNodeResult splitDivOrPResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
aParentDivOrP,
EditorRawDOMPoint(selNode, selOffset),
SplitAtEdges::eAllowToCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(splitDivOrPResult.Failed())) {
return splitDivOrPResult.Rv();
}
if (NS_WARN_IF(!splitDivOrPResult.DidSplit())) {
return NS_ERROR_FAILURE;
}
// Get rid of the break, if it is visible (otherwise it may be needed to
// prevent an empty p).
if (aNextBRNode && HTMLEditorRef().IsVisibleBRElement(aNextBRNode)) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*aNextBRNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Remove ID attribute on the paragraph from the existing right node.
rv = HTMLEditorRef().RemoveAttributeWithTransaction(aParentDivOrP,
*nsGkAtoms::id);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We need to ensure to both paragraphs visible even if they are empty.
// However, moz-<br> element isn't useful in this case because moz-<br>
// elements will be ignored by PlaintextSerializer. Additionally,
// moz-<br> will be exposed as <br> with Element.innerHTML. Therefore,
// we can use normal <br> elements for placeholder in this case.
// Note that Chromium also behaves so.
rv = InsertBRIfNeeded(*splitDivOrPResult.GetPreviousNode());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = InsertBRIfNeeded(*splitDivOrPResult.GetNextNode());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// selection to beginning of right hand para;
// look inside any containers that are up front.
nsIContent* child = HTMLEditorRef().GetLeftmostChild(&aParentDivOrP, true);
if (EditorBase::IsTextNode(child) || HTMLEditorRef().IsContainer(child)) {
EditorRawDOMPoint atStartOfChild(child, 0);
IgnoredErrorResult ignoredError;
SelectionRef().Collapse(atStartOfChild, ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to collapse selection at the end of the child");
} else {
EditorRawDOMPoint atChild(child);
IgnoredErrorResult ignoredError;
SelectionRef().Collapse(atChild, ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to collapse selection at the child");
}
return NS_OK;
}
nsresult
HTMLEditRules::ReturnInListItem(Element& aListItem,
nsINode& aNode,
int32_t aOffset)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItem));
// Get the item parent and the active editing host.
RefPtr<Element> host = HTMLEditorRef().GetActiveEditingHost();
// If we are in an empty item, then we want to pop up out of the list, but
// only if prefs say it's okay and if the parent isn't the active editing
// host.
if (mReturnInEmptyLIKillsList &&
host != aListItem.GetParentElement() &&
IsEmptyBlockElement(aListItem, IgnoreSingleBR::eYes)) {
nsCOMPtr<nsIContent> leftListNode = aListItem.GetParent();
// Are we the last list item in the list?
if (!HTMLEditorRef().IsLastEditableChild(&aListItem)) {
// We need to split the list!
EditorRawDOMPoint atListItem(&aListItem);
ErrorResult error;
leftListNode =
HTMLEditorRef().SplitNodeWithTransaction(atListItem, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
// Are we in a sublist?
EditorRawDOMPoint atNextSiblingOfLeftList(leftListNode);
DebugOnly<bool> advanced = atNextSiblingOfLeftList.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after the right list node");
if (HTMLEditUtils::IsList(atNextSiblingOfLeftList.GetContainer())) {
// If so, move item out of this list and into the grandparent list
nsresult rv =
HTMLEditorRef().MoveNodeWithTransaction(aListItem,
atNextSiblingOfLeftList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
ErrorResult error;
SelectionRef().Collapse(RawRangeBoundary(&aListItem, 0), error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
} else {
// Otherwise kill this item
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(aListItem);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Time to insert a paragraph
nsAtom& paraAtom = DefaultParagraphSeparator();
// We want a wrapper even if we separate with <br>
RefPtr<Element> pNode =
HTMLEditorRef().CreateNodeWithTransaction(&paraAtom == nsGkAtoms::br ?
*nsGkAtoms::p : paraAtom,
atNextSiblingOfLeftList);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!pNode)) {
return NS_ERROR_FAILURE;
}
// Append a <br> to it
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(
SelectionRef(), EditorRawDOMPoint(pNode, 0));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
// Set selection to before the break
ErrorResult error;
SelectionRef().Collapse(EditorRawDOMPoint(pNode, 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;
}
// Else we want a new list item at the same list level. Get ws code to
// adjust any ws.
nsCOMPtr<nsINode> selNode = &aNode;
nsresult rv =
WSRunObject::PrepareToSplitAcrossBlocks(&HTMLEditorRef(),
address_of(selNode), &aOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (NS_WARN_IF(!selNode->IsContent())) {
return NS_ERROR_FAILURE;
}
// Now split the list item.
SplitNodeResult splitListItemResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
aListItem, EditorRawDOMPoint(selNode, aOffset),
SplitAtEdges::eAllowToCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(splitListItemResult.Succeeded(),
"Failed to split the list item");
// Hack: until I can change the damaged doc range code back to being
// extra-inclusive, I have to manually detect certain list items that may be
// left empty.
nsCOMPtr<nsIContent> prevItem =
HTMLEditorRef().GetPriorHTMLSibling(&aListItem);
if (prevItem && HTMLEditUtils::IsListItem(prevItem)) {
bool isEmptyNode;
rv = HTMLEditorRef().IsEmptyNode(prevItem, &isEmptyNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (isEmptyNode) {
CreateElementResult createMozBrResult =
CreateMozBR(EditorRawDOMPoint(prevItem, 0));
if (NS_WARN_IF(createMozBrResult.Failed())) {
return createMozBrResult.Rv();
}
} else {
rv = HTMLEditorRef().IsEmptyNode(&aListItem, &isEmptyNode, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (isEmptyNode) {
RefPtr<nsAtom> nodeAtom = aListItem.NodeInfo()->NameAtom();
if (nodeAtom == nsGkAtoms::dd || nodeAtom == nsGkAtoms::dt) {
nsCOMPtr<nsINode> list = aListItem.GetParentNode();
int32_t itemOffset = list ? list->ComputeIndexOf(&aListItem) : -1;
nsAtom* listAtom = nodeAtom == nsGkAtoms::dt ? nsGkAtoms::dd
: nsGkAtoms::dt;
MOZ_DIAGNOSTIC_ASSERT(itemOffset != -1);
EditorRawDOMPoint atNextListItem(list, aListItem.GetNextSibling(),
itemOffset + 1);
RefPtr<Element> newListItem =
HTMLEditorRef().CreateNodeWithTransaction(*listAtom,
atNextListItem);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!newListItem)) {
return NS_ERROR_FAILURE;
}
rv = HTMLEditorRef().DeleteNodeWithTransaction(aListItem);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
ErrorResult error;
SelectionRef().Collapse(EditorRawDOMPoint(newListItem, 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;
}
RefPtr<Element> brElement;
nsresult rv =
HTMLEditorRef().CopyLastEditableChildStylesWithTransaction(
*prevItem->AsElement(), aListItem,
address_of(brElement));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_ERROR_FAILURE;
}
if (brElement) {
EditorRawDOMPoint atBrNode(brElement);
if (NS_WARN_IF(!atBrNode.IsSetAndValid())) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
SelectionRef().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;
}
} else {
WSRunObject wsObj(&HTMLEditorRef(), &aListItem, 0);
nsCOMPtr<nsINode> visNode;
int32_t visOffset = 0;
WSType wsType;
wsObj.NextVisibleNode(EditorRawDOMPoint(&aListItem, 0),
address_of(visNode), &visOffset, &wsType);
if (wsType == WSType::special || wsType == WSType::br ||
visNode->IsHTMLElement(nsGkAtoms::hr)) {
EditorRawDOMPoint atVisNode(visNode);
if (NS_WARN_IF(!atVisNode.IsSetAndValid())) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
SelectionRef().Collapse(atVisNode, 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;
}
rv = SelectionRef().Collapse(visNode, visOffset);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
}
}
ErrorResult error;
SelectionRef().Collapse(EditorRawDOMPoint(&aListItem, 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;
}
nsresult
HTMLEditRules::MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray)
{
MOZ_ASSERT(IsEditorDataAvailable());
// The idea here is to put the nodes into a minimal number of blockquotes.
// When the user blockquotes something, they expect one blockquote. That may
// not be possible (for instance, if they have two table cells selected, you
// need two blockquotes inside the cells).
RefPtr<Element> curBlock;
nsCOMPtr<nsINode> prevParent;
for (auto& curNode : aNodeArray) {
// Get the node to act on, and its location
if (NS_WARN_IF(!curNode->IsContent())) {
return NS_ERROR_FAILURE;
}
// If the node is a table element or list item, dive inside
if (HTMLEditUtils::IsTableElementButNotTable(curNode) ||
HTMLEditUtils::IsListItem(curNode)) {
// Forget any previous block
curBlock = nullptr;
// Recursion time
nsTArray<OwningNonNull<nsINode>> childArray;
GetChildNodesForOperation(*curNode, childArray);
nsresult rv = MakeBlockquote(childArray);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// If the node has different parent than previous node, further nodes in a
// new parent
if (prevParent) {
if (prevParent != curNode->GetParentNode()) {
// Forget any previous blockquote node we were using
curBlock = nullptr;
prevParent = curNode->GetParentNode();
}
} else {
prevParent = curNode->GetParentNode();
}
// If no curBlock, make one
if (!curBlock) {
EditorDOMPoint atCurNode(curNode);
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::blockquote,
atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curBlock =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::blockquote,
splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curBlock)) {
return NS_ERROR_FAILURE;
}
// remember our new block for postprocessing
mNewBlock = curBlock;
// note: doesn't matter if we set mNewBlock multiple times.
}
nsresult rv =
HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray)
{
MOZ_ASSERT(IsEditorDataAvailable());
// Intent of this routine is to be used for converting to/from headers,
// paragraphs, pre, and address. Those blocks that pretty much just contain
// inline things...
nsCOMPtr<Element> curBlock;
nsCOMPtr<nsIContent> firstNode, lastNode;
for (auto& curNode : aNodeArray) {
// If curNode is an <address>, <p>, <hn>, or <pre>, remove it.
if (HTMLEditUtils::IsFormatNode(curNode)) {
// Process any partial progress saved
if (curBlock) {
SplitRangeOffFromNodeResult removeMiddleContainerResult =
SplitRangeOffFromBlockAndRemoveMiddleContainer(*curBlock,
*firstNode, *lastNode);
if (NS_WARN_IF(removeMiddleContainerResult.Failed())) {
return removeMiddleContainerResult.Rv();
}
firstNode = lastNode = curBlock = nullptr;
}
if (!HTMLEditorRef().IsEditable(curNode)) {
continue;
}
// Remove current block
nsresult rv =
HTMLEditorRef().RemoveBlockContainerWithTransaction(
*curNode->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// XXX How about, <th>, <thead>, <tfoot>, <dt>, <dl>?
if (curNode->IsAnyOfHTMLElements(nsGkAtoms::table,
nsGkAtoms::tr,
nsGkAtoms::tbody,
nsGkAtoms::td,
nsGkAtoms::li,
nsGkAtoms::blockquote,
nsGkAtoms::div) ||
HTMLEditUtils::IsList(curNode)) {
// Process any partial progress saved
if (curBlock) {
SplitRangeOffFromNodeResult removeMiddleContainerResult =
SplitRangeOffFromBlockAndRemoveMiddleContainer(*curBlock,
*firstNode, *lastNode);
if (NS_WARN_IF(removeMiddleContainerResult.Failed())) {
return removeMiddleContainerResult.Rv();
}
firstNode = lastNode = curBlock = nullptr;
}
if (!HTMLEditorRef().IsEditable(curNode)) {
continue;
}
// Recursion time
nsTArray<OwningNonNull<nsINode>> childArray;
GetChildNodesForOperation(*curNode, childArray);
nsresult rv = RemoveBlockStyle(childArray);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
if (IsInlineNode(curNode)) {
if (curBlock) {
// If so, is this node a descendant?
if (EditorUtils::IsDescendantOf(*curNode, *curBlock)) {
// Then we don't need to do anything different for this node
lastNode = curNode->AsContent();
continue;
}
// Otherwise, we have progressed beyond end of curBlock, so let's
// handle it now. We need to remove the portion of curBlock that
// contains [firstNode - lastNode].
SplitRangeOffFromNodeResult removeMiddleContainerResult =
SplitRangeOffFromBlockAndRemoveMiddleContainer(*curBlock,
*firstNode, *lastNode);
if (NS_WARN_IF(removeMiddleContainerResult.Failed())) {
return removeMiddleContainerResult.Rv();
}
firstNode = lastNode = curBlock = nullptr;
// Fall out and handle curNode
}
curBlock = HTMLEditorRef().GetBlockNodeParent(curNode);
if (!curBlock || !HTMLEditUtils::IsFormatNode(curBlock) ||
!HTMLEditorRef().IsEditable(curBlock)) {
// Not a block kind that we care about.
curBlock = nullptr;
} else {
firstNode = lastNode = curNode->AsContent();
}
continue;
}
if (curBlock) {
// Some node that is already sans block style. Skip over it and process
// any partial progress saved.
SplitRangeOffFromNodeResult removeMiddleContainerResult =
SplitRangeOffFromBlockAndRemoveMiddleContainer(*curBlock,
*firstNode, *lastNode);
if (NS_WARN_IF(removeMiddleContainerResult.Failed())) {
return removeMiddleContainerResult.Rv();
}
firstNode = lastNode = curBlock = nullptr;
continue;
}
}
// Process any partial progress saved
if (curBlock) {
SplitRangeOffFromNodeResult removeMiddleContainerResult =
SplitRangeOffFromBlockAndRemoveMiddleContainer(*curBlock,
*firstNode, *lastNode);
if (NS_WARN_IF(removeMiddleContainerResult.Failed())) {
return removeMiddleContainerResult.Rv();
}
firstNode = lastNode = curBlock = nullptr;
}
return NS_OK;
}
nsresult
HTMLEditRules::ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
nsAtom& aBlockTag)
{
// Intent of this routine is to be used for converting to/from headers,
// paragraphs, pre, and address. Those blocks that pretty much just contain
// inline things...
MOZ_ASSERT(IsEditorDataAvailable());
nsCOMPtr<Element> newBlock;
nsCOMPtr<Element> curBlock;
for (auto& curNode : aNodeArray) {
EditorDOMPoint atCurNode(curNode);
// Is it already the right kind of block, or an uneditable block?
if (curNode->IsHTMLElement(&aBlockTag) ||
(!HTMLEditorRef().IsEditable(curNode) && IsBlockNode(curNode))) {
// Forget any previous block used for previous inline nodes
curBlock = nullptr;
// Do nothing to this block
continue;
}
// If curNode is a address, p, header, address, or pre, replace it with a
// new block of correct type.
// XXX: pre can't hold everything the others can
if (HTMLEditUtils::IsMozDiv(curNode) ||
HTMLEditUtils::IsFormatNode(curNode)) {
// Forget any previous block used for previous inline nodes
curBlock = nullptr;
newBlock =
HTMLEditorRef().ReplaceContainerAndCloneAttributesWithTransaction(
*curNode->AsElement(), aBlockTag);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!newBlock)) {
return NS_ERROR_FAILURE;
}
continue;
}
if (HTMLEditUtils::IsTable(curNode) ||
HTMLEditUtils::IsList(curNode) ||
curNode->IsAnyOfHTMLElements(nsGkAtoms::tbody,
nsGkAtoms::tr,
nsGkAtoms::td,
nsGkAtoms::li,
nsGkAtoms::blockquote,
nsGkAtoms::div)) {
// Forget any previous block used for previous inline nodes
curBlock = nullptr;
// Recursion time
nsTArray<OwningNonNull<nsINode>> childArray;
GetChildNodesForOperation(*curNode, childArray);
if (!childArray.IsEmpty()) {
nsresult rv = ApplyBlockStyle(childArray, aBlockTag);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// Make sure we can put a block here
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(aBlockTag, atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
RefPtr<Element> theBlock =
HTMLEditorRef().CreateNodeWithTransaction(aBlockTag,
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;
continue;
}
if (curNode->IsHTMLElement(nsGkAtoms::br)) {
// If the node is a break, we honor it by putting further nodes in a new
// parent
if (curBlock) {
// Forget any previous block used for previous inline nodes
curBlock = nullptr;
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(*curNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// The break is the first (or even only) node we encountered. Create a
// block for it.
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(aBlockTag, atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curBlock =
HTMLEditorRef().CreateNodeWithTransaction(aBlockTag,
splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curBlock)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
mNewBlock = curBlock;
// Note: doesn't matter if we set mNewBlock multiple times.
nsresult rv =
HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
if (IsInlineNode(curNode)) {
// If curNode is inline, pull it into curBlock. Note: it's assumed that
// consecutive inline nodes in aNodeArray are actually members of the
// same block parent. This happens to be true now as a side effect of
// how aNodeArray is contructed, but some additional logic should be
// added here if that should change
//
// If curNode is a non editable, drop it if we are going to <pre>.
if (&aBlockTag == nsGkAtoms::pre &&
!HTMLEditorRef().IsEditable(curNode)) {
// Do nothing to this block
continue;
}
// If no curBlock, make one
if (!curBlock) {
AutoEditorDOMPointOffsetInvalidator lockChild(atCurNode);
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(aBlockTag, atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curBlock =
HTMLEditorRef().CreateNodeWithTransaction(
aBlockTag, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curBlock)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
mNewBlock = curBlock;
// Note: doesn't matter if we set mNewBlock multiple times.
}
if (NS_WARN_IF(!atCurNode.IsSet())) {
// This is possible due to mutation events, let's not assert
return NS_ERROR_UNEXPECTED;
}
// XXX If curNode is a br, replace it with a return if going to <pre>
// This is a continuation of some inline nodes that belong together in
// the same block item. Use curBlock.
nsresult rv =
HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curBlock);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
return NS_OK;
}
template<typename PT, typename CT>
SplitNodeResult
HTMLEditRules::MaybeSplitAncestorsForInsertWithTransaction(
nsAtom& aTag,
const EditorDOMPointBase<PT, CT>& aStartOfDeepestRightNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aStartOfDeepestRightNode.IsSet())) {
return SplitNodeResult(NS_ERROR_INVALID_ARG);
}
MOZ_ASSERT(aStartOfDeepestRightNode.IsSetAndValid());
RefPtr<Element> host = HTMLEditorRef().GetActiveEditingHost();
if (NS_WARN_IF(!host)) {
return SplitNodeResult(NS_ERROR_FAILURE);
}
// The point must be descendant of editing host.
if (NS_WARN_IF(aStartOfDeepestRightNode.GetContainer() != host &&
!EditorUtils::IsDescendantOf(
*aStartOfDeepestRightNode.GetContainer(), *host))) {
return SplitNodeResult(NS_ERROR_INVALID_ARG);
}
// Look for a node that can legally contain the tag.
EditorRawDOMPoint pointToInsert(aStartOfDeepestRightNode);
for (; pointToInsert.IsSet();
pointToInsert.Set(pointToInsert.GetContainer())) {
// We cannot split active editing host and its ancestor. So, there is
// no element to contain the specified element.
if (NS_WARN_IF(pointToInsert.GetChild() == host)) {
return SplitNodeResult(NS_ERROR_FAILURE);
}
if (HTMLEditorRef().CanContainTag(*pointToInsert.GetContainer(), aTag)) {
// Found an ancestor node which can contain the element.
break;
}
}
MOZ_DIAGNOSTIC_ASSERT(pointToInsert.IsSet());
// If the point itself can contain the tag, we don't need to split any
// ancestor nodes. In this case, we should return the given split point
// as is.
if (pointToInsert.GetContainer() == aStartOfDeepestRightNode.GetContainer()) {
return SplitNodeResult(aStartOfDeepestRightNode);
}
SplitNodeResult splitNodeResult =
HTMLEditorRef().SplitNodeDeepWithTransaction(
*pointToInsert.GetChild(),
aStartOfDeepestRightNode,
SplitAtEdges::eAllowToCreateEmptyContainer);
if (NS_WARN_IF(!CanHandleEditAction())) {
return SplitNodeResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(splitNodeResult.Succeeded(),
"Failed to split the node for insert the element");
return splitNodeResult;
}
nsresult
HTMLEditRules::JoinNearestEditableNodesWithTransaction(
nsIContent& aNodeLeft,
nsIContent& aNodeRight,
EditorDOMPoint* aNewFirstChildOfRightNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aNewFirstChildOfRightNode);
// Caller responsible for left and right node being the same type
nsCOMPtr<nsINode> parent = aNodeLeft.GetParentNode();
if (NS_WARN_IF(!parent)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsINode> rightParent = aNodeRight.GetParentNode();
// If they don't have the same parent, first move the right node to after the
// left one
if (parent != rightParent) {
int32_t parOffset = parent->ComputeIndexOf(&aNodeLeft);
nsresult rv =
HTMLEditorRef().MoveNodeWithTransaction(
aNodeRight, EditorRawDOMPoint(parent, parOffset));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
EditorDOMPoint ret(&aNodeRight, aNodeLeft.Length());
// Separate join rules for differing blocks
if (HTMLEditUtils::IsList(&aNodeLeft) || aNodeLeft.GetAsText()) {
// For lists, merge shallow (wouldn't want to combine list items)
nsresult rv =
HTMLEditorRef().JoinNodesWithTransaction(aNodeLeft, aNodeRight);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
*aNewFirstChildOfRightNode = std::move(ret);
return NS_OK;
}
// Remember the last left child, and first right child
nsCOMPtr<nsIContent> lastLeft =
HTMLEditorRef().GetLastEditableChild(aNodeLeft);
if (NS_WARN_IF(!lastLeft)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIContent> firstRight =
HTMLEditorRef().GetFirstEditableChild(aNodeRight);
if (NS_WARN_IF(!firstRight)) {
return NS_ERROR_FAILURE;
}
// For list items, divs, etc., merge smart
nsresult rv = HTMLEditorRef().JoinNodesWithTransaction(aNodeLeft, aNodeRight);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (lastLeft && firstRight &&
HTMLEditorRef().AreNodesSameType(lastLeft, firstRight) &&
(lastLeft->GetAsText() ||
(lastLeft->IsElement() && firstRight->IsElement() &&
CSSEditUtils::ElementsSameStyle(lastLeft->AsElement(),
firstRight->AsElement())))) {
nsresult rv =
JoinNearestEditableNodesWithTransaction(*lastLeft, *firstRight,
aNewFirstChildOfRightNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
*aNewFirstChildOfRightNode = std::move(ret);
return NS_OK;
}
Element*
HTMLEditRules::GetTopEnclosingMailCite(nsINode& aNode)
{
nsCOMPtr<Element> ret;
for (nsCOMPtr<nsINode> node = &aNode; node; node = node->GetParentNode()) {
if ((IsPlaintextEditor() && node->IsHTMLElement(nsGkAtoms::pre)) ||
HTMLEditUtils::IsMailCite(node)) {
ret = node->AsElement();
}
if (node->IsHTMLElement(nsGkAtoms::body)) {
break;
}
}
return ret;
}
nsresult
HTMLEditRules::CacheInlineStyles(nsINode* aNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aNode)) {
return NS_ERROR_INVALID_ARG;
}
nsresult rv = GetInlineStyles(aNode, mCachedStyles);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::GetInlineStyles(nsINode* aNode,
StyleCache aStyleCache[SIZE_STYLE_TABLE])
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aNode);
bool useCSS = HTMLEditorRef().IsCSSEnabled();
for (size_t j = 0; j < SIZE_STYLE_TABLE; ++j) {
// If type-in state is set, don't intervene
bool typeInSet, unused;
HTMLEditorRef().mTypeInState->GetTypingState(
typeInSet, unused, aStyleCache[j].tag,
aStyleCache[j].attr, nullptr);
if (typeInSet) {
continue;
}
bool isSet = false;
nsAutoString outValue;
// Don't use CSS for <font size>, we don't support it usefully (bug 780035)
if (!useCSS || (aStyleCache[j].tag == nsGkAtoms::font &&
aStyleCache[j].attr == nsGkAtoms::size)) {
isSet =
HTMLEditorRef().IsTextPropertySetByContent(aNode, aStyleCache[j].tag,
aStyleCache[j].attr,
nullptr, &outValue);
} else {
isSet = CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(
aNode, aStyleCache[j].tag, aStyleCache[j].attr, outValue,
CSSEditUtils::eComputed);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
}
if (isSet) {
aStyleCache[j].mPresent = true;
aStyleCache[j].value.Assign(outValue);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::ReapplyCachedStyles()
{
MOZ_ASSERT(IsEditorDataAvailable());
// The idea here is to examine our cached list of styles and see if any have
// been removed. If so, add typeinstate for them, so that they will be
// reinserted when new content is added.
// remember if we are in css mode
bool useCSS = HTMLEditorRef().IsCSSEnabled();
if (!SelectionRef().RangeCount()) {
// Nothing to do
return NS_OK;
}
const RangeBoundary& atStartOfSelection =
SelectionRef().GetRangeAt(0)->StartRef();
nsCOMPtr<nsIContent> selNode =
atStartOfSelection.Container() &&
atStartOfSelection.Container()->IsContent() ?
atStartOfSelection.Container()->AsContent() : nullptr;
if (!selNode) {
// Nothing to do
return NS_OK;
}
StyleCache styleAtInsertionPoint[SIZE_STYLE_TABLE];
InitStyleCacheArray(styleAtInsertionPoint);
nsresult rv = GetInlineStyles(selNode, styleAtInsertionPoint);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv == NS_ERROR_EDITOR_DESTROYED ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
}
for (size_t i = 0; i < SIZE_STYLE_TABLE; ++i) {
if (mCachedStyles[i].mPresent) {
bool bFirst, bAny, bAll;
bFirst = bAny = bAll = false;
nsAutoString curValue;
if (useCSS) {
// check computed style first in css case
bAny = CSSEditUtils::IsCSSEquivalentToHTMLInlineStyleSet(
selNode, mCachedStyles[i].tag, mCachedStyles[i].attr, curValue,
CSSEditUtils::eComputed);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
}
if (!bAny) {
// then check typeinstate and html style
nsresult rv =
HTMLEditorRef().GetInlinePropertyBase(*mCachedStyles[i].tag,
mCachedStyles[i].attr,
&(mCachedStyles[i].value),
&bFirst, &bAny, &bAll,
&curValue);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// This style has disappeared through deletion. Let's add the styles to
// mTypeInState when same style isn't applied to the node already.
if ((!bAny || IsStyleCachePreservingSubAction(mTopLevelEditSubAction)) &&
(!styleAtInsertionPoint[i].mPresent ||
styleAtInsertionPoint[i].value != mCachedStyles[i].value)) {
HTMLEditorRef().mTypeInState->SetProp(mCachedStyles[i].tag,
mCachedStyles[i].attr,
mCachedStyles[i].value);
}
}
}
return NS_OK;
}
void
HTMLEditRules::ClearCachedStyles()
{
// clear the mPresent bits in mCachedStyles array
for (size_t j = 0; j < SIZE_STYLE_TABLE; j++) {
mCachedStyles[j].mPresent = false;
mCachedStyles[j].value.Truncate();
}
}
nsresult
HTMLEditRules::InsertBRElementToEmptyListItemsAndTableCellsInChangedRange()
{
MOZ_ASSERT(IsEditorDataAvailable());
// Gather list of empty nodes
nsTArray<OwningNonNull<nsINode>> nodeArray;
EmptyEditableFunctor functor(&HTMLEditorRef());
DOMIterator iter;
if (NS_WARN_IF(NS_FAILED(iter.Init(*mDocChangeRange)))) {
return NS_ERROR_FAILURE;
}
iter.AppendList(functor, nodeArray);
// Put moz-br's into these empty li's and td's
for (auto& node : nodeArray) {
// Need to put br at END of node. It may have empty containers in it and
// still pass the "IsEmptyNode" test, and we want the br's to be after
// them. Also, we want the br to be after the selection if the selection
// is in this node.
EditorRawDOMPoint endOfNode;
endOfNode.SetToEndOf(node);
// XXX This method should return nsreuslt due to may be destroyed by this
// CreateMozBr() call.
CreateElementResult createMozBrResult = CreateMozBR(endOfNode);
if (NS_WARN_IF(createMozBrResult.Failed())) {
return createMozBrResult.Rv();
}
}
return NS_OK;
}
nsresult
HTMLEditRules::AdjustWhitespace()
{
MOZ_ASSERT(IsEditorDataAvailable());
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(&SelectionRef()));
if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// Ask whitespace object to tweak nbsp's
nsresult rv =
WSRunObject(&HTMLEditorRef(), selectionStartPoint).AdjustWhitespace();
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::PinSelectionToNewBlock()
{
MOZ_ASSERT(IsEditorDataAvailable());
if (!SelectionRef().IsCollapsed()) {
return NS_OK;
}
if (NS_WARN_IF(!mNewBlock)) {
return NS_ERROR_NULL_POINTER;
}
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(&SelectionRef()));
if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// Use ranges and nsRange::CompareNodeToRange() to compare selection start
// to new block.
// XXX It's too expensive to use nsRange and set it only for comparing a
// DOM point with a node.
RefPtr<nsRange> range = new nsRange(selectionStartPoint.GetContainer());
nsresult rv = range->CollapseTo(selectionStartPoint);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
bool nodeBefore, nodeAfter;
rv = nsRange::CompareNodeToRange(mNewBlock, range, &nodeBefore, &nodeAfter);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (nodeBefore && nodeAfter) {
return NS_OK; // selection is inside block
}
if (nodeBefore) {
// selection is after block. put at end of block.
nsCOMPtr<nsINode> tmp = HTMLEditorRef().GetLastEditableChild(*mNewBlock);
if (!tmp) {
tmp = mNewBlock;
}
EditorRawDOMPoint endPoint;
if (EditorBase::IsTextNode(tmp) ||
HTMLEditorRef().IsContainer(tmp)) {
endPoint.SetToEndOf(tmp);
} else {
endPoint.Set(tmp);
if (NS_WARN_IF(!endPoint.AdvanceOffset())) {
return NS_ERROR_FAILURE;
}
}
ErrorResult error;
SelectionRef().Collapse(endPoint, 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;
}
// selection is before block. put at start of block.
nsCOMPtr<nsINode> tmp = HTMLEditorRef().GetFirstEditableChild(*mNewBlock);
if (!tmp) {
tmp = mNewBlock;
}
EditorRawDOMPoint atStartOfBlock;
if (EditorBase::IsTextNode(tmp) ||
HTMLEditorRef().IsContainer(tmp)) {
atStartOfBlock.Set(tmp);
} else {
atStartOfBlock.Set(tmp, 0);
}
ErrorResult error;
SelectionRef().Collapse(atStartOfBlock, 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;
}
void
HTMLEditRules::CheckInterlinePosition()
{
MOZ_ASSERT(IsEditorDataAvailable());
// If the selection isn't collapsed, do nothing.
if (!SelectionRef().IsCollapsed()) {
return;
}
// Get the (collapsed) selection location
nsRange* firstRange = SelectionRef().GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return;
}
EditorDOMPoint atStartOfSelection(firstRange->StartRef());
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return;
}
MOZ_ASSERT(atStartOfSelection.IsSetAndValid());
// First, let's check to see if we are after a <br>. We take care of this
// special-case first so that we don't accidentally fall through into one of
// the other conditionals.
nsCOMPtr<nsIContent> node =
HTMLEditorRef().GetPreviousEditableHTMLNodeInBlock(atStartOfSelection);
if (node && node->IsHTMLElement(nsGkAtoms::br)) {
IgnoredErrorResult ignoredError;
SelectionRef().SetInterlinePosition(true, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to set interline position");
return;
}
// Are we after a block? If so try set caret to following content
if (atStartOfSelection.GetChild()) {
node = HTMLEditorRef().GetPriorHTMLSibling(atStartOfSelection.GetChild());
} else {
node = nullptr;
}
if (node && IsBlockNode(*node)) {
IgnoredErrorResult ignoredError;
SelectionRef().SetInterlinePosition(true, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to set interline position");
return;
}
// Are we before a block? If so try set caret to prior content
if (atStartOfSelection.GetChild()) {
node = HTMLEditorRef().GetNextHTMLSibling(atStartOfSelection.GetChild());
} else {
node = nullptr;
}
if (node && IsBlockNode(*node)) {
IgnoredErrorResult ignoredError;
SelectionRef().SetInterlinePosition(false, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to unset interline position");
}
}
nsresult
HTMLEditRules::AdjustSelection(nsIEditor::EDirection aAction)
{
MOZ_ASSERT(IsEditorDataAvailable());
// if the selection isn't collapsed, do nothing.
// moose: one thing to do instead is check for the case of
// only a single break selected, and collapse it. Good thing? Beats me.
if (!SelectionRef().IsCollapsed()) {
return NS_OK;
}
// get the (collapsed) selection location
EditorDOMPoint point(EditorBase::GetStartPoint(&SelectionRef()));
if (NS_WARN_IF(!point.IsSet())) {
return NS_ERROR_FAILURE;
}
// are we in an editable node?
while (!HTMLEditorRef().IsEditable(point.GetContainer())) {
// scan up the tree until we find an editable place to be
point.Set(point.GetContainer());
if (NS_WARN_IF(!point.IsSet())) {
return NS_ERROR_FAILURE;
}
}
// make sure we aren't in an empty block - user will see no cursor. If this
// is happening, put a <br> in the block if allowed.
RefPtr<Element> theblock = HTMLEditorRef().GetBlock(*point.GetContainer());
if (theblock && HTMLEditorRef().IsEditable(theblock)) {
bool isEmptyNode;
nsresult rv =
HTMLEditorRef().IsEmptyNode(theblock, &isEmptyNode, false, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// check if br can go into the destination node
if (isEmptyNode &&
HTMLEditorRef().CanContainTag(*point.GetContainer(), *nsGkAtoms::br)) {
Element* rootElement = HTMLEditorRef().GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_FAILURE;
}
if (point.GetContainer() == rootElement) {
// Our root node is completely empty. Don't add a <br> here.
// AfterEditInner() will add one for us when it calls
// CreateBogusNodeIfNeeded()!
return NS_OK;
}
// we know we can skip the rest of this routine given the cirumstance
CreateElementResult createMozBrResult = CreateMozBR(point);
if (NS_WARN_IF(createMozBrResult.Failed())) {
return createMozBrResult.Rv();
}
return NS_OK;
}
}
// are we in a text node?
if (point.IsInTextNode()) {
return NS_OK; // we LIKE it when we are in a text node. that RULZ
}
// do we need to insert a special mozBR? We do if we are:
// 1) prior node is in same block where selection is AND
// 2) prior node is a br AND
// 3) that br is not visible
nsCOMPtr<nsIContent> nearNode =
HTMLEditorRef().GetPreviousEditableHTMLNode(point);
if (nearNode) {
// is nearNode also a descendant of same block?
RefPtr<Element> block = HTMLEditorRef().GetBlock(*point.GetContainer());
RefPtr<Element> nearBlock = HTMLEditorRef().GetBlockNodeParent(nearNode);
if (block && block == nearBlock) {
if (nearNode && TextEditUtils::IsBreak(nearNode)) {
if (!HTMLEditorRef().IsVisibleBRElement(nearNode)) {
// need to insert special moz BR. Why? Because if we don't
// the user will see no new line for the break. Also, things
// like table cells won't grow in height.
CreateElementResult createMozBrResult = CreateMozBR(point);
if (NS_WARN_IF(createMozBrResult.Failed())) {
return createMozBrResult.Rv();
}
point.Set(createMozBrResult.GetNewNode());
// selection stays *before* moz-br, sticking to it
ErrorResult error;
SelectionRef().SetInterlinePosition(true, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!error.Failed(),
"Failed to set interline position");
error = NS_OK;
SelectionRef().Collapse(point, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
} else {
nsCOMPtr<nsIContent> nextNode =
HTMLEditorRef().GetNextEditableHTMLNodeInBlock(*nearNode);
if (nextNode && TextEditUtils::IsMozBR(nextNode)) {
// selection between br and mozbr. make it stick to mozbr
// so that it will be on blank line.
IgnoredErrorResult ignoredError;
SelectionRef().SetInterlinePosition(true, ignoredError);
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to set interline position");
}
}
}
}
}
// we aren't in a textnode: are we adjacent to text or a break or an image?
nearNode = HTMLEditorRef().GetPreviousEditableHTMLNodeInBlock(point);
if (nearNode && (TextEditUtils::IsBreak(nearNode) ||
EditorBase::IsTextNode(nearNode) ||
HTMLEditUtils::IsImage(nearNode) ||
nearNode->IsHTMLElement(nsGkAtoms::hr))) {
// this is a good place for the caret to be
return NS_OK;
}
nearNode = HTMLEditorRef().GetNextEditableHTMLNodeInBlock(point);
if (nearNode && (TextEditUtils::IsBreak(nearNode) ||
EditorBase::IsTextNode(nearNode) ||
nearNode->IsAnyOfHTMLElements(nsGkAtoms::img,
nsGkAtoms::hr))) {
return NS_OK; // this is a good place for the caret to be
}
// look for a nearby text node.
// prefer the correct direction.
nearNode = FindNearEditableNode(point, aAction);
if (!nearNode) {
return NS_OK;
}
EditorDOMPoint pt = GetGoodSelPointForNode(*nearNode, aAction);
ErrorResult error;
SelectionRef().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;
}
template<typename PT, typename CT>
nsIContent*
HTMLEditRules::FindNearEditableNode(const EditorDOMPointBase<PT, CT>& aPoint,
nsIEditor::EDirection aDirection)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!aPoint.IsSet())) {
return nullptr;
}
MOZ_ASSERT(aPoint.IsSetAndValid());
nsIContent* nearNode = nullptr;
if (aDirection == nsIEditor::ePrevious) {
nearNode = HTMLEditorRef().GetPreviousEditableHTMLNode(aPoint);
if (!nearNode) {
return nullptr; // Not illegal.
}
} else {
nearNode = HTMLEditorRef().GetNextEditableHTMLNode(aPoint);
if (NS_WARN_IF(!nearNode)) {
// Perhaps, illegal because the node pointed by aPoint isn't editable
// and nobody of previous nodes is editable.
return nullptr;
}
}
// scan in the right direction until we find an eligible text node,
// but don't cross any breaks, images, or table elements.
// XXX This comment sounds odd. |nearNode| may have already crossed breaks
// and/or images.
while (nearNode && !(EditorBase::IsTextNode(nearNode) ||
TextEditUtils::IsBreak(nearNode) ||
HTMLEditUtils::IsImage(nearNode))) {
if (aDirection == nsIEditor::ePrevious) {
nearNode = HTMLEditorRef().GetPreviousEditableHTMLNode(*nearNode);
if (NS_WARN_IF(!nearNode)) {
return nullptr;
}
} else {
nearNode = HTMLEditorRef().GetNextEditableHTMLNode(*nearNode);
if (NS_WARN_IF(!nearNode)) {
return nullptr;
}
}
}
// don't cross any table elements
if (InDifferentTableElements(nearNode, aPoint.GetContainer())) {
return nullptr;
}
// otherwise, ok, we have found a good spot to put the selection
return nearNode;
}
bool
HTMLEditRules::InDifferentTableElements(nsINode* aNode1,
nsINode* aNode2)
{
MOZ_ASSERT(aNode1 && aNode2);
while (aNode1 && !HTMLEditUtils::IsTableElement(aNode1)) {
aNode1 = aNode1->GetParentNode();
}
while (aNode2 && !HTMLEditUtils::IsTableElement(aNode2)) {
aNode2 = aNode2->GetParentNode();
}
return aNode1 != aNode2;
}
nsresult
HTMLEditRules::RemoveEmptyNodesInChangedRange()
{
MOZ_ASSERT(IsEditorDataAvailable());
// Some general notes on the algorithm used here: the goal is to examine all
// the nodes in mDocChangeRange, and remove the empty ones. We do this by
// using a content iterator to traverse all the nodes in the range, and
// placing the empty nodes into an array. After finishing the iteration, we
// delete the empty nodes in the array. (They cannot be deleted as we find
// them because that would invalidate the iterator.)
//
// Since checking to see if a node is empty can be costly for nodes with many
// descendants, there are some optimizations made. I rely on the fact that
// the iterator is post-order: it will visit children of a node before
// visiting the parent node. So if I find that a child node is not empty, I
// know that its parent is not empty without even checking. So I put the
// parent on a "skipList" which is just a voidArray of nodes I can skip the
// empty check on. If I encounter a node on the skiplist, i skip the
// processing for that node and replace its slot in the skiplist with that
// node's parent.
//
// An interesting idea is to go ahead and regard parent nodes that are NOT on
// the skiplist as being empty (without even doing the IsEmptyNode check) on
// the theory that if they weren't empty, we would have encountered a
// non-empty child earlier and thus put this parent node on the skiplist.
//
// Unfortunately I can't use that strategy here, because the range may
// include some children of a node while excluding others. Thus I could find
// all the _examined_ children empty, but still not have an empty parent.
// need an iterator
nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator();
nsresult rv = iter->Init(mDocChangeRange);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsTArray<OwningNonNull<nsINode>> arrayOfEmptyNodes, arrayOfEmptyCites, skipList;
// Check for empty nodes
while (!iter->IsDone()) {
OwningNonNull<nsINode> node = *iter->GetCurrentNode();
nsCOMPtr<nsINode> parent = node->GetParentNode();
size_t idx = skipList.IndexOf(node);
if (idx != skipList.NoIndex) {
// This node is on our skip list. Skip processing for this node, and
// replace its value in the skip list with the value of its parent
if (parent) {
skipList[idx] = parent;
}
} else {
bool isCandidate = false;
bool isEmptyNode = false;
bool isMailCite = false;
if (node->IsElement()) {
if (node->IsHTMLElement(nsGkAtoms::body)) {
// Don't delete the body
} else if ((isMailCite = HTMLEditUtils::IsMailCite(node)) ||
node->IsHTMLElement(nsGkAtoms::a) ||
HTMLEditUtils::IsInlineStyle(node) ||
HTMLEditUtils::IsList(node) ||
node->IsHTMLElement(nsGkAtoms::div)) {
// Only consider certain nodes to be empty for purposes of removal
isCandidate = true;
} else if (HTMLEditUtils::IsFormatNode(node) ||
HTMLEditUtils::IsListItem(node) ||
node->IsHTMLElement(nsGkAtoms::blockquote)) {
// These node types are candidates if selection is not in them. If
// it is one of these, don't delete if selection inside. This is so
// we can create empty headings, etc., for the user to type into.
bool isSelectionEndInNode;
rv = SelectionEndpointInNode(node, &isSelectionEndInNode);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!isSelectionEndInNode) {
isCandidate = true;
}
}
}
if (isCandidate) {
// We delete mailcites even if they have a solo br in them. Other
// nodes we require to be empty.
rv = HTMLEditorRef().IsEmptyNode(node, &isEmptyNode,
isMailCite, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (isEmptyNode) {
if (isMailCite) {
// mailcites go on a separate list from other empty nodes
arrayOfEmptyCites.AppendElement(*node);
} else {
arrayOfEmptyNodes.AppendElement(*node);
}
}
}
if (!isEmptyNode && parent) {
// put parent on skip list
skipList.AppendElement(*parent);
}
}
iter->Next();
}
// now delete the empty nodes
for (OwningNonNull<nsINode>& delNode : arrayOfEmptyNodes) {
if (HTMLEditorRef().IsModifiableNode(delNode)) {
rv = HTMLEditorRef().DeleteNodeWithTransaction(*delNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
// Now delete the empty mailcites. This is a separate step because we want
// to pull out any br's and preserve them.
for (OwningNonNull<nsINode>& delNode : arrayOfEmptyCites) {
bool isEmptyNode;
rv = HTMLEditorRef().IsEmptyNode(delNode, &isEmptyNode, false, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!isEmptyNode) {
// We are deleting a cite that has just a br. We want to delete cite,
// but preserve br.
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(
SelectionRef(), EditorRawDOMPoint(delNode));
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
}
rv = HTMLEditorRef().DeleteNodeWithTransaction(*delNode);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::SelectionEndpointInNode(nsINode* aNode,
bool* aResult)
{
MOZ_ASSERT(IsEditorDataAvailable());
NS_ENSURE_TRUE(aNode && aResult, NS_ERROR_NULL_POINTER);
*aResult = false;
uint32_t rangeCount = SelectionRef().RangeCount();
for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
RefPtr<nsRange> range = SelectionRef().GetRangeAt(rangeIdx);
nsINode* startContainer = range->GetStartContainer();
if (startContainer) {
if (aNode == startContainer) {
*aResult = true;
return NS_OK;
}
if (EditorUtils::IsDescendantOf(*startContainer, *aNode)) {
*aResult = true;
return NS_OK;
}
}
nsINode* endContainer = range->GetEndContainer();
if (startContainer == endContainer) {
continue;
}
if (endContainer) {
if (aNode == endContainer) {
*aResult = true;
return NS_OK;
}
if (EditorUtils::IsDescendantOf(*endContainer, *aNode)) {
*aResult = true;
return NS_OK;
}
}
}
return NS_OK;
}
bool
HTMLEditRules::IsEmptyInline(nsINode& aNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (IsInlineNode(aNode) && HTMLEditorRef().IsContainer(&aNode)) {
bool isEmpty = true;
HTMLEditorRef().IsEmptyNode(&aNode, &isEmpty);
return isEmpty;
}
return false;
}
bool
HTMLEditRules::ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes)
{
MOZ_ASSERT(IsEditorDataAvailable());
// We have a list of nodes which we are candidates for being moved into a new
// block. Determine if it's anything more than a blank line. Look for
// editable content above and beyond one single BR.
if (NS_WARN_IF(!aArrayOfNodes.Length())) {
return true;
}
int32_t brCount = 0;
for (auto& node : aArrayOfNodes) {
if (!HTMLEditorRef().IsEditable(node)) {
continue;
}
if (TextEditUtils::IsBreak(node)) {
// First break doesn't count
if (brCount) {
return false;
}
brCount++;
} else if (IsEmptyInline(node)) {
// Empty inline, keep looking
} else {
return false;
}
}
return true;
}
nsresult
HTMLEditRules::PopListItem(nsIContent& aListItem,
bool* aOutOfList)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (aOutOfList) {
*aOutOfList = false;
}
if (NS_WARN_IF(!aListItem.GetParent()) ||
NS_WARN_IF(!aListItem.GetParent()->GetParentNode()) ||
!HTMLEditUtils::IsListItem(&aListItem)) {
return NS_ERROR_FAILURE;
}
// if it's first or last list item, don't need to split the list
// otherwise we do.
bool isFirstListItem = HTMLEditorRef().IsFirstEditableChild(&aListItem);
bool isLastListItem = HTMLEditorRef().IsLastEditableChild(&aListItem);
nsCOMPtr<nsIContent> leftListNode = aListItem.GetParent();
// If it's at middle of parent list element, split the parent list element.
// Then, aListItem becomes the first list item of the right list element.
nsCOMPtr<nsIContent> listItem(&aListItem);
if (!isFirstListItem && !isLastListItem) {
EditorDOMPoint atListItem(listItem);
if (NS_WARN_IF(!atListItem.IsSet())) {
return NS_ERROR_INVALID_ARG;
}
MOZ_ASSERT(atListItem.IsSetAndValid());
ErrorResult error;
leftListNode = HTMLEditorRef().SplitNodeWithTransaction(atListItem, error);
if (NS_WARN_IF(!CanHandleEditAction())) {
error.SuppressException();
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
// In most cases, insert the list item into the new left list node..
EditorDOMPoint pointToInsertListItem(leftListNode);
if (NS_WARN_IF(!pointToInsertListItem.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(pointToInsertListItem.IsSetAndValid());
// But when the list item was the first child of the right list, it should
// be inserted between the both list elements. This allows user to hit
// Enter twice at a list item breaks the parent list node.
if (!isFirstListItem) {
DebugOnly<bool> advanced = pointToInsertListItem.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to right list node");
}
nsresult rv =
HTMLEditorRef().MoveNodeWithTransaction(*listItem, pointToInsertListItem);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// unwrap list item contents if they are no longer in a list
// XXX If the parent list element is a child of another list element
// (although invalid tree), the list item element won't be unwrapped.
// That makes the parent ancestor element tree valid, but might be
// unexpected result.
// XXX If aListItem is <dl> or <dd> and current parent is <ul> or <ol>,
// the list items won't be unwrapped. If aListItem is <li> and its
// current parent is <dl>, there is same issue.
if (!HTMLEditUtils::IsList(pointToInsertListItem.GetContainer()) &&
HTMLEditUtils::IsListItem(listItem)) {
rv = HTMLEditorRef().RemoveBlockContainerWithTransaction(
*listItem->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (aOutOfList) {
*aOutOfList = true;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::RemoveListStructure(Element& aListElement)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(HTMLEditUtils::IsList(&aListElement));
while (aListElement.GetFirstChild()) {
OwningNonNull<nsIContent> child = *aListElement.GetFirstChild();
if (HTMLEditUtils::IsListItem(child)) {
bool isOutOfList;
// Keep popping it out until it's not in a list anymore
// XXX Using PopuListItem() is too expensive for this purpose. Looks
// like the reason why this method uses it is, only this loop
// wants to work with first child of aList. However, what it
// actually does is removing <li> as container. So, just using
// RemoveBlockContainerWithTransaction() is reasonable.
// XXX This loop means that if aListElement is is a child of another
// list element (although it's invalid tree), this moves the
// list item to outside of aListElement's parent. Is that really
// intentional behavior?
do {
nsresult rv = PopListItem(child, &isOutOfList);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} while (!isOutOfList);
continue;
}
if (HTMLEditUtils::IsList(child)) {
nsresult rv = RemoveListStructure(*child->AsElement());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
continue;
}
// Delete any non-list items for now
// XXX This is not HTML5 aware. HTML5 allows all list elements to have
// <script> and <template> and <dl> element to have <div> to group
// some <dt> and <dd> elements. So, this may break valid children.
nsresult rv = HTMLEditorRef().DeleteNodeWithTransaction(*child);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Delete the now-empty list
nsresult rv =
HTMLEditorRef().RemoveBlockContainerWithTransaction(aListElement);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::ConfirmSelectionInBody()
{
MOZ_ASSERT(IsEditorDataAvailable());
Element* rootElement = HTMLEditorRef().GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_UNEXPECTED;
}
EditorRawDOMPoint selectionStartPoint(
EditorBase::GetStartPoint(&SelectionRef()));
if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// Check that selection start container is inside the <body> element.
//XXXsmaug this code is insane.
nsINode* temp = selectionStartPoint.GetContainer();
while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
temp = temp->GetParentOrHostNode();
}
// If we aren't in the <body> element, force the issue.
if (!temp) {
IgnoredErrorResult ignoredError;
SelectionRef().Collapse(RawRangeBoundary(rootElement, 0), ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to collapse selection at start of the root element");
return NS_OK;
}
EditorRawDOMPoint selectionEndPoint(EditorBase::GetEndPoint(&SelectionRef()));
if (NS_WARN_IF(!selectionEndPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// check that selNode is inside body
//XXXsmaug this code is insane.
temp = selectionEndPoint.GetContainer();
while (temp && !temp->IsHTMLElement(nsGkAtoms::body)) {
temp = temp->GetParentOrHostNode();
}
// If we aren't in the <body> element, force the issue.
if (!temp) {
IgnoredErrorResult ignoredError;
SelectionRef().Collapse(RawRangeBoundary(rootElement, 0), ignoredError);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"Failed to collapse selection at start of the root element");
}
return NS_OK;
}
nsresult
HTMLEditRules::UpdateDocChangeRange(nsRange* aRange)
{
MOZ_ASSERT(IsEditorDataAvailable());
// first make sure aRange is in the document. It might not be if
// portions of our editting action involved manipulating nodes
// prior to placing them in the document (e.g., populating a list item
// before placing it in its list)
const RangeBoundary& atStart = aRange->StartRef();
if (NS_WARN_IF(!atStart.IsSet())) {
return NS_ERROR_FAILURE;
}
if (!HTMLEditorRef().IsDescendantOfRoot(atStart.Container())) {
// just return - we don't need to adjust mDocChangeRange in this case
return NS_OK;
}
if (!mDocChangeRange) {
// clone aRange.
mDocChangeRange = aRange->CloneRange();
} else {
// compare starts of ranges
ErrorResult error;
int16_t result =
mDocChangeRange->CompareBoundaryPoints(Range_Binding::START_TO_START,
*aRange, error);
if (error.ErrorCodeIs(NS_ERROR_NOT_INITIALIZED)) {
// This will happen is mDocChangeRange is non-null, but the range is
// uninitialized. In this case we'll set the start to aRange start.
// The same test won't be needed further down since after we've set
// the start the range will be collapsed to that point.
result = 1;
error.SuppressException();
}
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
// Positive result means mDocChangeRange start is after aRange start.
if (result > 0) {
mDocChangeRange->SetStart(atStart.AsRaw(), error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
// compare ends of ranges
result =
mDocChangeRange->CompareBoundaryPoints(Range_Binding::END_TO_END,
*aRange, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
// Negative result means mDocChangeRange end is before aRange end.
if (result < 0) {
const RangeBoundary& atEnd = aRange->EndRef();
if (NS_WARN_IF(!atEnd.IsSet())) {
return NS_ERROR_FAILURE;
}
mDocChangeRange->SetEnd(atEnd.AsRaw(), error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
}
}
return NS_OK;
}
nsresult
HTMLEditRules::InsertBRIfNeededInternal(nsINode& aNode,
bool aInsertMozBR)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (!IsBlockNode(aNode)) {
return NS_OK;
}
bool isEmpty;
nsresult rv = HTMLEditorRef().IsEmptyNode(&aNode, &isEmpty);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!isEmpty) {
return NS_OK;
}
CreateElementResult createBrResult =
!aInsertMozBR ? CreateBR(EditorRawDOMPoint(&aNode, 0)) :
CreateMozBR(EditorRawDOMPoint(&aNode, 0));
if (NS_WARN_IF(createBrResult.Failed())) {
return createBrResult.Rv();
}
return NS_OK;
}
void
HTMLEditRules::DidCreateNode(Selection& aSelection,
Element& aNewElement)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
// assumption that Join keeps the righthand node
IgnoredErrorResult ignoredError;
mUtilRange->SelectNode(aNewElement, ignoredError);
if (NS_WARN_IF(ignoredError.Failed())) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::DidInsertNode(Selection& aSelection,
nsIContent& aContent)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
IgnoredErrorResult ignoredError;
mUtilRange->SelectNode(aContent, ignoredError);
if (NS_WARN_IF(ignoredError.Failed())) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::WillDeleteNode(Selection& aSelection,
nsINode& aChild)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
IgnoredErrorResult ignoredError;
mUtilRange->SelectNode(aChild, ignoredError);
if (NS_WARN_IF(ignoredError.Failed())) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::DidSplitNode(Selection& aSelection,
nsINode& aExistingRightNode,
nsINode& aNewLeftNode)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
nsresult rv = mUtilRange->SetStartAndEnd(&aNewLeftNode, 0,
&aExistingRightNode, 0);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::WillJoinNodes(nsINode& aLeftNode,
nsINode& aRightNode)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
// remember split point
mJoinOffset = aLeftNode.Length();
}
void
HTMLEditRules::DidJoinNodes(Selection& aSelection,
nsINode& aLeftNode,
nsINode& aRightNode)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
// assumption that Join keeps the righthand node
nsresult rv = mUtilRange->CollapseTo(&aRightNode, mJoinOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::DidInsertText(Selection& aSelection,
nsINode& aTextNode,
int32_t aOffset,
const nsAString& aString)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
int32_t length = aString.Length();
nsresult rv = mUtilRange->SetStartAndEnd(&aTextNode, aOffset,
&aTextNode, aOffset + length);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::DidDeleteText(Selection& aSelection,
nsINode& aTextNode,
int32_t aOffset,
int32_t aLength)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
nsresult rv = mUtilRange->CollapseTo(&aTextNode, aOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
void
HTMLEditRules::WillDeleteSelection(Selection& aSelection)
{
if (!mListenerEnabled) {
return;
}
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, aSelection);
EditorRawDOMPoint startPoint = EditorBase::GetStartPoint(&SelectionRef());
if (NS_WARN_IF(!startPoint.IsSet())) {
return;
}
EditorRawDOMPoint endPoint = EditorBase::GetEndPoint(&SelectionRef());
if (NS_WARN_IF(!endPoint.IsSet())) {
return;
}
nsresult rv = mUtilRange->SetStartAndEnd(startPoint, endPoint);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
UpdateDocChangeRange(mUtilRange);
}
nsresult
HTMLEditRules::RemoveAlignment(nsINode& aNode,
const nsAString& aAlignType,
bool aDescendantsOnly)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (EditorBase::IsTextNode(&aNode) || HTMLEditUtils::IsTable(&aNode)) {
return NS_OK;
}
nsCOMPtr<nsINode> child, tmp;
if (aDescendantsOnly) {
child = aNode.GetFirstChild();
} else {
child = &aNode;
}
bool useCSS = HTMLEditorRef().IsCSSEnabled();
// Let's remove all alignment hints in the children of aNode; it can
// be an ALIGN attribute (in case we just remove it) or a CENTER
// element (here we have to remove the container and keep its
// children). We break on tables and don't look at their children.
while (child) {
if (aDescendantsOnly) {
// get the next sibling right now because we could have to remove child
tmp = child->GetNextSibling();
} else {
tmp = nullptr;
}
if (child->IsHTMLElement(nsGkAtoms::center)) {
// the current node is a CENTER element
// first remove children's alignment
nsresult rv = RemoveAlignment(*child, aAlignType, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// we may have to insert BRs in first and last position of element's children
// if the nodes before/after are not blocks and not BRs
rv = MakeSureElemStartsAndEndsOnCR(*child);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// now remove the CENTER container
rv = HTMLEditorRef().RemoveContainerWithTransaction(*child->AsElement());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else if (IsBlockNode(*child) || child->IsHTMLElement(nsGkAtoms::hr)) {
// the current node is a block element
if (HTMLEditUtils::SupportsAlignAttr(*child)) {
// remove the ALIGN attribute if this element can have it
nsresult rv =
HTMLEditorRef().RemoveAttributeWithTransaction(*child->AsElement(),
*nsGkAtoms::align);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (useCSS) {
if (child->IsAnyOfHTMLElements(nsGkAtoms::table, nsGkAtoms::hr)) {
nsresult rv =
HTMLEditorRef().SetAttributeOrEquivalent(child->AsElement(),
nsGkAtoms::align,
aAlignType, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
nsAutoString dummyCssValue;
nsresult rv = HTMLEditorRef().mCSSEditUtils->RemoveCSSInlineStyle(
*child,
nsGkAtoms::textAlign,
dummyCssValue);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
if (!child->IsHTMLElement(nsGkAtoms::table)) {
// unless this is a table, look at children
nsresult rv = RemoveAlignment(*child, aAlignType, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
child = tmp;
}
return NS_OK;
}
nsresult
HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsINode& aNode,
bool aStarts)
{
MOZ_ASSERT(IsEditorDataAvailable());
nsINode* child = aStarts ? HTMLEditorRef().GetFirstEditableChild(aNode) :
HTMLEditorRef().GetLastEditableChild(aNode);
if (NS_WARN_IF(!child)) {
return NS_OK;
}
bool foundCR = false;
if (IsBlockNode(*child) || child->IsHTMLElement(nsGkAtoms::br)) {
foundCR = true;
} else {
nsINode* sibling =
aStarts ? HTMLEditorRef().GetPriorHTMLSibling(&aNode) :
HTMLEditorRef().GetNextHTMLSibling(&aNode);
if (sibling) {
if (IsBlockNode(*sibling) || sibling->IsHTMLElement(nsGkAtoms::br)) {
foundCR = true;
}
} else {
foundCR = true;
}
}
if (!foundCR) {
EditorRawDOMPoint pointToInsert;
if (!aStarts) {
pointToInsert.SetToEndOf(&aNode);
} else {
pointToInsert.Set(&aNode, 0);
}
RefPtr<Element> brElement =
HTMLEditorRef().InsertBrElementWithTransaction(SelectionRef(),
pointToInsert);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!brElement)) {
return NS_ERROR_FAILURE;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::MakeSureElemStartsAndEndsOnCR(nsINode& aNode)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
nsresult rv = MakeSureElemStartsOrEndsOnCR(aNode, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = MakeSureElemStartsOrEndsOnCR(aNode, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::AlignBlock(Element& aElement,
const nsAString& aAlignType,
ResetAlignOf aResetAlignOf)
{
MOZ_ASSERT(IsEditorDataAvailable());
if (!IsBlockNode(aElement) && !aElement.IsHTMLElement(nsGkAtoms::hr)) {
// We deal only with blocks; early way out
return NS_OK;
}
nsresult rv = RemoveAlignment(aElement, aAlignType,
aResetAlignOf == ResetAlignOf::OnlyDescendants);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (HTMLEditorRef().IsCSSEnabled()) {
// Let's use CSS alignment; we use margin-left and margin-right for tables
// and text-align for other block-level elements
nsresult rv =
HTMLEditorRef().SetAttributeOrEquivalent(&aElement, nsGkAtoms::align,
aAlignType, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// HTML case; this code is supposed to be called ONLY if the element
// supports the align attribute but we'll never know...
if (NS_WARN_IF(!HTMLEditUtils::SupportsAlignAttr(aElement))) {
// XXX error?
return NS_OK;
}
rv = HTMLEditorRef().SetAttributeOrEquivalent(&aElement, nsGkAtoms::align,
aAlignType, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::ChangeMarginStart(Element& aElement,
bool aIncrease)
{
MOZ_ASSERT(IsEditorDataAvailable());
nsAtom& marginProperty = MarginPropertyAtomForIndent(aElement);
nsAutoString value;
CSSEditUtils::GetSpecifiedProperty(aElement, marginProperty, value);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
float f;
RefPtr<nsAtom> unit;
CSSEditUtils::ParseLength(value, &f, getter_AddRefs(unit));
if (!f) {
nsAutoString defaultLengthUnit;
CSSEditUtils::GetDefaultLengthUnit(defaultLengthUnit);
unit = NS_Atomize(defaultLengthUnit);
}
int8_t multiplier = aIncrease ? +1 : -1;
if (nsGkAtoms::in == unit) {
f += NS_EDITOR_INDENT_INCREMENT_IN * multiplier;
} else if (nsGkAtoms::cm == unit) {
f += NS_EDITOR_INDENT_INCREMENT_CM * multiplier;
} else if (nsGkAtoms::mm == unit) {
f += NS_EDITOR_INDENT_INCREMENT_MM * multiplier;
} else if (nsGkAtoms::pt == unit) {
f += NS_EDITOR_INDENT_INCREMENT_PT * multiplier;
} else if (nsGkAtoms::pc == unit) {
f += NS_EDITOR_INDENT_INCREMENT_PC * multiplier;
} else if (nsGkAtoms::em == unit) {
f += NS_EDITOR_INDENT_INCREMENT_EM * multiplier;
} else if (nsGkAtoms::ex == unit) {
f += NS_EDITOR_INDENT_INCREMENT_EX * multiplier;
} else if (nsGkAtoms::px == unit) {
f += NS_EDITOR_INDENT_INCREMENT_PX * multiplier;
} else if (nsGkAtoms::percentage == unit) {
f += NS_EDITOR_INDENT_INCREMENT_PERCENT * multiplier;
}
if (0 < f) {
nsAutoString newValue;
newValue.AppendFloat(f);
newValue.Append(nsDependentAtomString(unit));
HTMLEditorRef().mCSSEditUtils->SetCSSProperty(aElement, marginProperty,
newValue);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
return NS_OK;
}
HTMLEditorRef().mCSSEditUtils->RemoveCSSProperty(aElement, marginProperty,
value);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
// Remove unnecessary divs
if (!aElement.IsHTMLElement(nsGkAtoms::div) ||
&aElement == HTMLEditorRef().GetActiveEditingHost() ||
!HTMLEditorRef().IsDescendantOfEditorRoot(&aElement) ||
HTMLEditor::HasAttributes(&aElement)) {
return NS_OK;
}
nsresult rv = HTMLEditorRef().RemoveContainerWithTransaction(aElement);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::WillAbsolutePosition(bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aCancel && aHandled);
// 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;
RefPtr<Element> focusElement = HTMLEditorRef().GetSelectionContainer();
if (focusElement && HTMLEditUtils::IsImage(focusElement)) {
mNewBlock = focusElement;
return NS_OK;
}
rv = NormalizeSelection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = PrepareToMakeElementAbsolutePosition(aHandled, address_of(mNewBlock));
// PrepareToMakeElementAbsolutePosition() may restore selection with
// AutoSelectionRestorer. Therefore, the editor might have already been
// destroyed now.
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::PrepareToMakeElementAbsolutePosition(
bool* aHandled,
RefPtr<Element>* aTargetElement)
{
MOZ_ASSERT(IsEditorDataAvailable());
MOZ_ASSERT(aHandled);
MOZ_ASSERT(aTargetElement);
AutoSelectionRestorer selectionRestorer(&SelectionRef(), &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::eSetPositionToAbsolute);
// Use these ranges to contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
EditSubAction::eSetPositionToAbsolute,
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 = SelectionRef().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> positionedDiv =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::div,
splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!positionedDiv)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
*aTargetElement = positionedDiv;
// 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
*aHandled = true;
// Don't restore the selection
selectionRestorer.Abort();
ErrorResult error;
SelectionRef().Collapse(RawRangeBoundary(positionedDiv, 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;
}
// Okay, now go through all the nodes and put them in a blockquote, or
// whatever is appropriate. Woohoo!
nsCOMPtr<Element> curList, curPositionedDiv, 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())) {
return NS_ERROR_FAILURE; // XXX not continue??
}
// Ignore all non-editable nodes. Leave them be.
if (!HTMLEditorRef().IsEditable(curNode)) {
continue;
}
nsCOMPtr<nsIContent> sibling;
// Some logic for putting list items into nested lists...
if (HTMLEditUtils::IsList(atCurNode.GetContainer())) {
// 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(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();
}
if (!curPositionedDiv) {
curPositionedDiv =
HTMLEditorRef().CreateNodeWithTransaction(
*nsGkAtoms::div, splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(curPositionedDiv,
"Failed to create current positioned div element");
*aTargetElement = curPositionedDiv;
}
EditorRawDOMPoint atEndOfCurPositionedDiv;
atEndOfCurPositionedDiv.SetToEndOf(curPositionedDiv);
curList =
HTMLEditorRef().CreateNodeWithTransaction(*containerName,
atEndOfCurPositionedDiv);
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.
}
// 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;
}
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(!atListItem.IsSet())) {
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();
}
if (!curPositionedDiv) {
EditorRawDOMPoint atListItemParent(atListItem.GetContainer());
curPositionedDiv =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::div,
atListItemParent);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(curPositionedDiv,
"Failed to create current positioned div element");
*aTargetElement = curPositionedDiv;
}
EditorRawDOMPoint atEndOfCurPositionedDiv;
atEndOfCurPositionedDiv.SetToEndOf(curPositionedDiv);
curList =
HTMLEditorRef().CreateNodeWithTransaction(*containerName,
atEndOfCurPositionedDiv);
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 div to put things in if we haven't already
if (!curPositionedDiv) {
if (curNode->IsHTMLElement(nsGkAtoms::div)) {
curPositionedDiv = curNode->AsElement();
*aTargetElement = curPositionedDiv;
curList = nullptr;
continue;
}
SplitNodeResult splitNodeResult =
MaybeSplitAncestorsForInsertWithTransaction(*nsGkAtoms::div,
atCurNode);
if (NS_WARN_IF(splitNodeResult.Failed())) {
return splitNodeResult.Rv();
}
curPositionedDiv =
HTMLEditorRef().CreateNodeWithTransaction(*nsGkAtoms::div,
splitNodeResult.SplitPoint());
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(!curPositionedDiv)) {
return NS_ERROR_FAILURE;
}
// Remember our new block for postprocessing
*aTargetElement = curPositionedDiv;
// curPositionedDiv is now the correct thing to put curNode in
}
// Tuck the node into the end of the active blockquote
rv = HTMLEditorRef().MoveNodeToEndWithTransaction(*curNode->AsContent(),
*curPositionedDiv);
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::DidAbsolutePosition()
{
MOZ_ASSERT(IsEditorDataAvailable());
if (!mNewBlock) {
return NS_OK;
}
nsresult rv = HTMLEditorRef().SetPositionToAbsoluteOrStatic(*mNewBlock, true);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::WillRemoveAbsolutePosition(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;
RefPtr<Element> element =
HTMLEditorRef().GetAbsolutelyPositionedSelectionContainer();
if (NS_WARN_IF(!element)) {
return NS_ERROR_FAILURE;
}
{
AutoSelectionRestorer selectionRestorer(&SelectionRef(), &HTMLEditorRef());
nsresult rv = HTMLEditorRef().SetPositionToAbsoluteOrStatic(*element, false);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Restoring Selection might cause destroying the HTML editor.
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
return NS_OK;
}
nsresult
HTMLEditRules::WillRelativeChangeZIndex(int32_t aChange,
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;
RefPtr<Element> element =
HTMLEditorRef().GetAbsolutelyPositionedSelectionContainer();
if (NS_WARN_IF(!element)) {
return NS_ERROR_FAILURE;
}
{
AutoSelectionRestorer selectionRestorer(&SelectionRef(), &HTMLEditorRef());
int32_t zIndex;
nsresult rv = HTMLEditorRef().RelativeChangeElementZIndex(*element, aChange,
&zIndex);
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Restoring Selection might cause destroying the HTML editor.
if (NS_WARN_IF(!CanHandleEditAction())) {
return NS_ERROR_EDITOR_DESTROYED;
}
return NS_OK;
}
nsresult
HTMLEditRules::DocumentModified()
{
nsContentUtils::AddScriptRunner(
NewRunnableMethod("HTMLEditRules::DocumentModifiedWorker",
this,
&HTMLEditRules::DocumentModifiedWorker));
// Be aware, if DocumentModifiedWorker() is called synchronously, the
// editor might have been destroyed here.
return NS_OK;
}
void
HTMLEditRules::DocumentModifiedWorker()
{
if (NS_WARN_IF(!CanHandleEditAction())) {
return;
}
Selection* selection = mHTMLEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return;
}
AutoSafeEditorData setData(*this, *mHTMLEditor, *selection);
// DeleteNodeWithTransaction() below may cause a flush, which could destroy
// the editor
nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
// Delete our bogus node, if we have one, since the document might not be
// empty any more.
if (mBogusNode) {
DebugOnly<nsresult> rv =
HTMLEditorRef().DeleteNodeWithTransaction(*mBogusNode);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to remove the bogus node");
mBogusNode = nullptr;
}
// Try to recreate the bogus node if needed.
DebugOnly<nsresult> rv = CreateBogusNodeIfNeeded();
NS_WARNING_ASSERTION(rv.value != NS_ERROR_EDITOR_DESTROYED,
"The editor has been destroyed during creating a bogus node");
}
} // namespace mozilla