gecko-dev/editor/libeditor/HTMLEditRules.cpp
Makoto Kato e1cb496376 Bug 1270235 - Part 1. Check parent node is null in RemoveEmptyNodes. r=masayuki
RemoveEmptyNodes doesn't check whether parent node is null.  So we shouldn't add null to skip list.

Also, although this crash depends on clang, if debug build, it will hit the assertion on all compiler.

MozReview-Commit-ID: GgiczVExlIn

--HG--
extra : rebase_source : 5ec0119a80d86dfeac5e970cdf540997cca24c34
2017-02-13 15:46:09 +09:00

8894 lines
310 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/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/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 "nsIAtom.h"
#include "nsIContent.h"
#include "nsIContentIterator.h"
#include "nsID.h"
#include "nsIDOMCharacterData.h"
#include "nsIDOMDocument.h"
#include "nsIDOMElement.h"
#include "nsIDOMNode.h"
#include "nsIDOMText.h"
#include "nsIFrame.h"
#include "nsIHTMLAbsPosEditor.h"
#include "nsIHTMLDocument.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 {
class RulesInfo;
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 IsBlockNode(const nsINode& node)
{
return HTMLEditor::NodeIsBlockStatic(&node);
}
static bool IsInlineNode(const nsINode& node)
{
return !IsBlockNode(node);
}
static bool
IsStyleCachePreservingAction(EditAction action)
{
return action == EditAction::deleteSelection ||
action == EditAction::insertBreak ||
action == EditAction::makeList ||
action == EditAction::indent ||
action == EditAction::outdent ||
action == EditAction::align ||
action == EditAction::makeBasicBlock ||
action == EditAction::removeList ||
action == EditAction::makeDefListItem ||
action == EditAction::insertElement ||
action == EditAction::insertQuotation;
}
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
{
return HTMLEditUtils::IsTableCell(aNode) ||
HTMLEditUtils::IsListItem(aNode);
}
};
class BRNodeFunctor final : public BoolDomIterFunctor
{
public:
virtual bool operator()(nsINode* aNode) const
{
return aNode->IsHTMLElement(nsGkAtoms::br);
}
};
class EmptyEditableFunctor final : public BoolDomIterFunctor
{
public:
explicit EmptyEditableFunctor(HTMLEditor* aHTMLEditor)
: mHTMLEditor(aHTMLEditor)
{}
virtual bool operator()(nsINode* aNode) const
{
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)
{
InitFields();
}
void
HTMLEditRules::InitFields()
{
mHTMLEditor = nullptr;
mDocChangeRange = nullptr;
mListenerEnabled = true;
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, EmptyString());
aStyleCache[1] = StyleCache(nsGkAtoms::i, EmptyString());
aStyleCache[2] = StyleCache(nsGkAtoms::u, EmptyString());
aStyleCache[3] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("face"));
aStyleCache[4] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("size"));
aStyleCache[5] = StyleCache(nsGkAtoms::font, NS_LITERAL_STRING("color"));
aStyleCache[6] = StyleCache(nsGkAtoms::tt, EmptyString());
aStyleCache[7] = StyleCache(nsGkAtoms::em, EmptyString());
aStyleCache[8] = StyleCache(nsGkAtoms::strong, EmptyString());
aStyleCache[9] = StyleCache(nsGkAtoms::dfn, EmptyString());
aStyleCache[10] = StyleCache(nsGkAtoms::code, EmptyString());
aStyleCache[11] = StyleCache(nsGkAtoms::samp, EmptyString());
aStyleCache[12] = StyleCache(nsGkAtoms::var, EmptyString());
aStyleCache[13] = StyleCache(nsGkAtoms::cite, EmptyString());
aStyleCache[14] = StyleCache(nsGkAtoms::abbr, EmptyString());
aStyleCache[15] = StyleCache(nsGkAtoms::acronym, EmptyString());
aStyleCache[16] = StyleCache(nsGkAtoms::backgroundColor, EmptyString());
aStyleCache[17] = StyleCache(nsGkAtoms::sub, EmptyString());
aStyleCache[18] = StyleCache(nsGkAtoms::sup, EmptyString());
}
HTMLEditRules::~HTMLEditRules()
{
// remove ourselves as a listener to edit actions
// In some cases, we have already been removed by
// ~HTMLEditor, in which case we will get a null pointer here
// which we ignore. But this allows us to add the ability to
// switch rule sets on the fly if we want.
if (mHTMLEditor) {
mHTMLEditor->RemoveEditActionListener(this);
}
}
NS_IMPL_ADDREF_INHERITED(HTMLEditRules, TextEditRules)
NS_IMPL_RELEASE_INHERITED(HTMLEditRules, TextEditRules)
NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLEditRules)
NS_INTERFACE_TABLE_INHERITED(HTMLEditRules, nsIEditActionListener)
NS_INTERFACE_TABLE_TAIL_INHERITING(TextEditRules)
NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLEditRules, TextEditRules,
mDocChangeRange, mUtilRange, mNewBlock,
mRangeItem)
NS_IMETHODIMP
HTMLEditRules::Init(TextEditor* aTextEditor)
{
InitFields();
mHTMLEditor = static_cast<HTMLEditor*>(aTextEditor);
// call through to base class Init
nsresult rv = TextEditRules::Init(aTextEditor);
NS_ENSURE_SUCCESS(rv, rv);
// cache any prefs we care about
static const char kPrefName[] =
"editor.html.typing.returnInEmptyListItemClosesList";
nsAdoptingCString returnInEmptyLIKillsList =
Preferences::GetCString(kPrefName);
// 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 = mHTMLEditor->GetRoot();
if (!node) {
node = mHTMLEditor->GetDocument();
}
NS_ENSURE_STATE(node);
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 rv;
mDocChangeRange->SelectNode(*node, rv);
NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
AdjustSpecialBreaks();
}
// add ourselves as a listener to edit actions
return mHTMLEditor->AddEditActionListener(this);
}
NS_IMETHODIMP
HTMLEditRules::DetachEditor()
{
if (mHTMLEditor) {
mHTMLEditor->RemoveEditActionListener(this);
}
mHTMLEditor = nullptr;
return TextEditRules::DetachEditor();
}
NS_IMETHODIMP
HTMLEditRules::BeforeEdit(EditAction action,
nsIEditor::EDirection aDirection)
{
if (mLockRulesSniffing) {
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
AutoLockRulesSniffing lockIt(this);
mDidExplicitlySetInterline = false;
if (!mActionNesting) {
mActionNesting++;
// Clear our flag about if just deleted a range
mDidRangedDelete = false;
// Remember where our selection was before edit action took place:
// Get selection
RefPtr<Selection> selection = htmlEditor->GetSelection();
// Get the selection location
if (NS_WARN_IF(!selection) || !selection->RangeCount()) {
return NS_ERROR_UNEXPECTED;
}
mRangeItem->startNode = selection->GetRangeAt(0)->GetStartParent();
mRangeItem->startOffset = selection->GetRangeAt(0)->StartOffset();
mRangeItem->endNode = selection->GetRangeAt(0)->GetEndParent();
mRangeItem->endOffset = selection->GetRangeAt(0)->EndOffset();
nsCOMPtr<nsINode> selStartNode = mRangeItem->startNode;
nsCOMPtr<nsINode> selEndNode = mRangeItem->endNode;
// Register with range updater to track this as we perturb the doc
htmlEditor->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 (action == EditAction::insertText ||
action == EditAction::insertIMEText ||
action == EditAction::deleteSelection ||
IsStyleCachePreservingAction(action)) {
nsCOMPtr<nsINode> selNode =
aDirection == nsIEditor::eNext ? selEndNode : selStartNode;
nsresult rv = CacheInlineStyles(GetAsDOMNode(selNode));
NS_ENSURE_SUCCESS(rv, rv);
}
// Stabilize the document against contenteditable count changes
nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument();
NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE);
if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
htmlDoc->ChangeContentEditableCount(nullptr, +1);
mRestoreContentEditableCount = true;
}
// Check that selection is in subtree defined by body node
ConfirmSelectionInBody();
// Let rules remember the top level action
mTheAction = action;
}
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::AfterEdit(EditAction action,
nsIEditor::EDirection aDirection)
{
if (mLockRulesSniffing) {
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
AutoLockRulesSniffing lockIt(this);
MOZ_ASSERT(mActionNesting > 0);
nsresult rv = NS_OK;
mActionNesting--;
if (!mActionNesting) {
// Do all the tricky stuff
rv = AfterEditInner(action, aDirection);
// Free up selectionState range item
htmlEditor->mRangeUpdater.DropRangeItem(mRangeItem);
// Reset the contenteditable count to its previous value
if (mRestoreContentEditableCount) {
nsCOMPtr<nsIDOMDocument> doc = htmlEditor->GetDOMDocument();
NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED);
nsCOMPtr<nsIHTMLDocument> htmlDoc = do_QueryInterface(doc);
NS_ENSURE_TRUE(htmlDoc, NS_ERROR_FAILURE);
if (htmlDoc->GetEditingState() == nsIHTMLDocument::eContentEditable) {
htmlDoc->ChangeContentEditableCount(nullptr, -1);
}
mRestoreContentEditableCount = false;
}
}
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
HTMLEditRules::AfterEditInner(EditAction action,
nsIEditor::EDirection aDirection)
{
ConfirmSelectionInBody();
if (action == EditAction::ignore) {
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
NS_ENSURE_STATE(selection);
nsCOMPtr<nsIDOMNode> rangeStartParent, rangeEndParent;
int32_t rangeStartOffset = 0, rangeEndOffset = 0;
// do we have a real range to act on?
bool bDamagedRange = false;
if (mDocChangeRange) {
mDocChangeRange->GetStartContainer(getter_AddRefs(rangeStartParent));
mDocChangeRange->GetEndContainer(getter_AddRefs(rangeEndParent));
mDocChangeRange->GetStartOffset(&rangeStartOffset);
mDocChangeRange->GetEndOffset(&rangeEndOffset);
if (rangeStartParent && rangeEndParent)
bDamagedRange = true;
}
if (bDamagedRange && !((action == EditAction::undo) ||
(action == EditAction::redo))) {
// 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.
NS_ENSURE_STATE(mHTMLEditor);
AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
// expand the "changed doc range" as needed
PromoteRange(*mDocChangeRange, action);
// 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 EditAction::loadHTML, EditAction::insertText, etc.
// That's why this is here rather than DidDeleteSelection().
if (action == EditAction::deleteSelection && mDidRangedDelete) {
nsresult rv = InsertBRIfNeeded(selection);
NS_ENSURE_SUCCESS(rv, rv);
}
// add in any needed <br>s, and remove any unneeded ones.
AdjustSpecialBreaks();
// merge any adjacent text nodes
if (action != EditAction::insertText &&
action != EditAction::insertIMEText) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->CollapseAdjacentTextNodes(mDocChangeRange);
NS_ENSURE_SUCCESS(rv, rv);
}
// clean up any empty nodes in the selection
nsresult rv = RemoveEmptyNodes();
NS_ENSURE_SUCCESS(rv, rv);
// attempt to transform any unneeded nbsp's into spaces after doing various operations
if (action == EditAction::insertText ||
action == EditAction::insertIMEText ||
action == EditAction::deleteSelection ||
action == EditAction::insertBreak ||
action == EditAction::htmlPaste ||
action == EditAction::loadHTML) {
rv = AdjustWhitespace(selection);
NS_ENSURE_SUCCESS(rv, rv);
// also do this for original selection endpoints.
NS_ENSURE_STATE(mHTMLEditor);
NS_ENSURE_STATE(mRangeItem->startNode);
NS_ENSURE_STATE(mRangeItem->endNode);
WSRunObject(mHTMLEditor, mRangeItem->startNode,
mRangeItem->startOffset).AdjustWhitespace();
// we only need to handle old selection endpoint if it was different from start
if (mRangeItem->startNode != mRangeItem->endNode ||
mRangeItem->startOffset != mRangeItem->endOffset) {
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject(mHTMLEditor, mRangeItem->endNode,
mRangeItem->endOffset).AdjustWhitespace();
}
}
// if we created a new block, make sure selection lands in it
if (mNewBlock) {
rv = PinSelectionToNewBlock(selection);
mNewBlock = nullptr;
}
// adjust selection for insert text, html paste, and delete actions
if (action == EditAction::insertText ||
action == EditAction::insertIMEText ||
action == EditAction::deleteSelection ||
action == EditAction::insertBreak ||
action == EditAction::htmlPaste ||
action == EditAction::loadHTML) {
rv = AdjustSelection(selection, aDirection);
NS_ENSURE_SUCCESS(rv, rv);
}
// check for any styles which were removed inappropriately
if (action == EditAction::insertText ||
action == EditAction::insertIMEText ||
action == EditAction::deleteSelection ||
IsStyleCachePreservingAction(action)) {
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->mTypeInState->UpdateSelState(selection);
rv = ReapplyCachedStyles();
NS_ENSURE_SUCCESS(rv, rv);
ClearCachedStyles();
}
}
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->HandleInlineSpellCheck(action, selection,
GetAsDOMNode(mRangeItem->startNode),
mRangeItem->startOffset,
rangeStartParent, rangeStartOffset,
rangeEndParent, rangeEndOffset);
NS_ENSURE_SUCCESS(rv, rv);
// detect empty doc
rv = CreateBogusNodeIfNeeded(selection);
NS_ENSURE_SUCCESS(rv, rv);
// adjust selection HINT if needed
if (!mDidExplicitlySetInterline) {
CheckInterlinePosition(*selection);
}
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::WillDoAction(Selection* aSelection,
RulesInfo* aInfo,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aInfo && aCancel && aHandled);
*aCancel = false;
*aHandled = false;
// my kingdom for dynamic cast
TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
// Deal with actions for which we don't need to check whether the selection is
// editable.
if (info->action == EditAction::outputText ||
info->action == EditAction::undo ||
info->action == EditAction::redo) {
return TextEditRules::WillDoAction(aSelection, aInfo, aCancel, aHandled);
}
// Nothing to do if there's no selection to act on
if (!aSelection) {
return NS_OK;
}
NS_ENSURE_TRUE(aSelection->RangeCount(), NS_OK);
RefPtr<nsRange> range = aSelection->GetRangeAt(0);
nsCOMPtr<nsINode> selStartNode = range->GetStartParent();
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsModifiableNode(selStartNode)) {
*aCancel = true;
return NS_OK;
}
nsCOMPtr<nsINode> selEndNode = range->GetEndParent();
if (selStartNode != selEndNode) {
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsModifiableNode(selEndNode)) {
*aCancel = true;
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsModifiableNode(range->GetCommonAncestor())) {
*aCancel = true;
return NS_OK;
}
}
switch (info->action) {
case EditAction::insertText:
case EditAction::insertIMEText:
UndefineCaretBidiLevel(aSelection);
return WillInsertText(info->action, aSelection, aCancel, aHandled,
info->inString, info->outString, info->maxLength);
case EditAction::loadHTML:
return WillLoadHTML(aSelection, aCancel);
case EditAction::insertBreak:
UndefineCaretBidiLevel(aSelection);
return WillInsertBreak(*aSelection, aCancel, aHandled);
case EditAction::deleteSelection:
return WillDeleteSelection(aSelection, info->collapsedAction,
info->stripWrappers, aCancel, aHandled);
case EditAction::makeList:
return WillMakeList(aSelection, info->blockType, info->entireList,
info->bulletType, aCancel, aHandled);
case EditAction::indent:
return WillIndent(aSelection, aCancel, aHandled);
case EditAction::outdent:
return WillOutdent(*aSelection, aCancel, aHandled);
case EditAction::setAbsolutePosition:
return WillAbsolutePosition(*aSelection, aCancel, aHandled);
case EditAction::removeAbsolutePosition:
return WillRemoveAbsolutePosition(aSelection, aCancel, aHandled);
case EditAction::align:
return WillAlign(*aSelection, *info->alignType, aCancel, aHandled);
case EditAction::makeBasicBlock:
return WillMakeBasicBlock(*aSelection, *info->blockType, aCancel,
aHandled);
case EditAction::removeList:
return WillRemoveList(aSelection, info->bOrdered, aCancel, aHandled);
case EditAction::makeDefListItem:
return WillMakeDefListItem(aSelection, info->blockType, info->entireList,
aCancel, aHandled);
case EditAction::insertElement:
WillInsert(*aSelection, aCancel);
return NS_OK;
case EditAction::decreaseZIndex:
return WillRelativeChangeZIndex(aSelection, -1, aCancel, aHandled);
case EditAction::increaseZIndex:
return WillRelativeChangeZIndex(aSelection, 1, aCancel, aHandled);
default:
return TextEditRules::WillDoAction(aSelection, aInfo,
aCancel, aHandled);
}
}
NS_IMETHODIMP
HTMLEditRules::DidDoAction(Selection* aSelection,
RulesInfo* aInfo,
nsresult aResult)
{
TextRulesInfo* info = static_cast<TextRulesInfo*>(aInfo);
switch (info->action) {
case EditAction::insertBreak:
return DidInsertBreak(aSelection, aResult);
case EditAction::deleteSelection:
return DidDeleteSelection(aSelection, info->collapsedAction, aResult);
case EditAction::makeBasicBlock:
case EditAction::indent:
case EditAction::outdent:
case EditAction::align:
return DidMakeBasicBlock(aSelection, aInfo, aResult);
case EditAction::setAbsolutePosition: {
nsresult rv = DidMakeBasicBlock(aSelection, aInfo, aResult);
NS_ENSURE_SUCCESS(rv, rv);
return DidAbsolutePosition();
}
default:
// pass through to TextEditRules
return TextEditRules::DidDoAction(aSelection, aInfo, aResult);
}
}
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;
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no,
TouchContent::no);
NS_ENSURE_SUCCESS(rv, 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;
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetListActionNodes(arrayOfNodes, EntireList::no,
TouchContent::no);
NS_ENSURE_SUCCESS(rv, 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);
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// For now, just return first alignment. We'll lie about if it's mixed.
// This is for efficiency given that our current ui doesn't care if it's
// mixed.
// cmanske: NOT TRUE! We would like to pay attention to mixed state in Format
// | Align submenu!
// This routine assumes that alignment is done ONLY via divs
// Default alignment is left
*aMixed = false;
*aAlign = nsIHTMLEditor::eLeft;
// Get selection
NS_ENSURE_STATE(htmlEditor->GetSelection());
OwningNonNull<Selection> selection = *htmlEditor->GetSelection();
// Get selection location
NS_ENSURE_TRUE(htmlEditor->GetRoot(), NS_ERROR_FAILURE);
OwningNonNull<Element> root = *htmlEditor->GetRoot();
int32_t rootOffset = root->GetParentNode() ?
root->GetParentNode()->IndexOf(root) : -1;
NS_ENSURE_STATE(selection->GetRangeAt(0) &&
selection->GetRangeAt(0)->GetStartParent());
OwningNonNull<nsINode> parent = *selection->GetRangeAt(0)->GetStartParent();
int32_t offset = selection->GetRangeAt(0)->StartOffset();
// Is the selection collapsed?
nsCOMPtr<nsINode> nodeToExamine;
if (selection->Collapsed() || parent->GetAsText()) {
// If selection is collapsed, we want to look at 'parent' and its ancestors
// for divs with alignment on them. If we are in a text node, then that is
// the node of interest.
nodeToExamine = parent;
} else if (parent->IsHTMLElement(nsGkAtoms::html) && offset == rootOffset) {
// If we have selected the body, let's look at the first editable node
nodeToExamine = htmlEditor->GetNextNode(parent, offset, true);
} else {
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(selection, arrayOfRanges, EditAction::align);
// Use these ranges to construct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
EditAction::align, TouchContent::no);
NS_ENSURE_SUCCESS(rv, rv);
nodeToExamine = arrayOfNodes.SafeElementAt(0);
}
NS_ENSURE_TRUE(nodeToExamine, NS_ERROR_NULL_POINTER);
nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(*nodeToExamine);
NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
if (htmlEditor->IsCSSEnabled() &&
htmlEditor->mCSSEditUtils->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
htmlEditor->mCSSEditUtils->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 (HTMLEditUtils::SupportsAlignAttr(GetAsDOMNode(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 nsIAtom& MarginPropertyAtomForIndent(CSSEditUtils& aHTMLCSSUtils,
nsINode& aNode)
{
nsAutoString direction;
aHTMLCSSUtils.GetComputedProperty(aNode, *nsGkAtoms::direction, direction);
return direction.EqualsLiteral("rtl") ?
*nsGkAtoms::marginRight : *nsGkAtoms::marginLeft;
}
nsresult
HTMLEditRules::GetIndentState(bool* aCanIndent,
bool* aCanOutdent)
{
// XXX Looks like that this is implementation of
// nsIHTMLEditor::getIndentState() however nobody calls this method
// even with the interface method.
NS_ENSURE_TRUE(aCanIndent && aCanOutdent, NS_ERROR_FAILURE);
*aCanIndent = true;
*aCanOutdent = false;
// get selection
NS_ENSURE_STATE(mHTMLEditor && mHTMLEditor->GetSelection());
OwningNonNull<Selection> selection = *mHTMLEditor->GetSelection();
// contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
nsresult rv = GetNodesFromSelection(*selection, EditAction::indent,
arrayOfNodes, TouchContent::no);
NS_ENSURE_SUCCESS(rv, rv);
// examine nodes in selection for blockquotes or list elements;
// these we can outdent. Note that we return true for canOutdent
// if *any* of the selection is outdentable, rather than all of it.
NS_ENSURE_STATE(mHTMLEditor);
bool useCSS = mHTMLEditor->IsCSSEnabled();
for (auto& curNode : Reversed(arrayOfNodes)) {
if (HTMLEditUtils::IsNodeThatCanOutdent(GetAsDOMNode(curNode))) {
*aCanOutdent = true;
break;
} else if (useCSS) {
// we are in CSS mode, indentation is done using the margin-left (or margin-right) property
NS_ENSURE_STATE(mHTMLEditor);
nsIAtom& marginProperty =
MarginPropertyAtomForIndent(*mHTMLEditor->mCSSEditUtils, curNode);
nsAutoString value;
// retrieve its specified value
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->mCSSEditUtils->GetSpecifiedProperty(*curNode,
marginProperty, value);
float f;
nsCOMPtr<nsIAtom> unit;
// get its number part and its unit
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
// if the number part is strictly positive, outdent is possible
if (0 < f) {
*aCanOutdent = true;
break;
}
}
}
if (!*aCanOutdent) {
// if we haven't found something to outdent yet, also check the parents
// of selection endpoints. We might have a blockquote or list item
// in the parent hierarchy.
// gather up info we need for test
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIDOMNode> parent, tmp, root = do_QueryInterface(mHTMLEditor->GetRoot());
NS_ENSURE_TRUE(root, NS_ERROR_NULL_POINTER);
int32_t selOffset;
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
// test start parent hierarchy
rv = EditorBase::GetStartNodeAndOffset(selection, getter_AddRefs(parent),
&selOffset);
NS_ENSURE_SUCCESS(rv, rv);
while (parent && parent != root) {
if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) {
*aCanOutdent = true;
break;
}
tmp = parent;
tmp->GetParentNode(getter_AddRefs(parent));
}
// test end parent hierarchy
rv = EditorBase::GetEndNodeAndOffset(selection, getter_AddRefs(parent),
&selOffset);
NS_ENSURE_SUCCESS(rv, rv);
while (parent && parent != root) {
if (HTMLEditUtils::IsNodeThatCanOutdent(parent)) {
*aCanOutdent = true;
break;
}
tmp = parent;
tmp->GetParentNode(getter_AddRefs(parent));
}
}
return NS_OK;
}
nsresult
HTMLEditRules::GetParagraphState(bool* aMixed,
nsAString& outFormat)
{
// This routine is *heavily* tied to our ui choices in the paragraph
// style popup. I can't see a way around that.
NS_ENSURE_TRUE(aMixed, NS_ERROR_NULL_POINTER);
*aMixed = true;
outFormat.Truncate(0);
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, TouchContent::no);
NS_ENSURE_SUCCESS(rv, 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);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// we might have an empty node list. if so, find selection parent
// and put that on the list
if (arrayOfNodes.IsEmpty()) {
nsCOMPtr<nsINode> selNode;
int32_t selOffset;
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
NS_ENSURE_STATE(selection);
rv = EditorBase::GetStartNodeAndOffset(selection, getter_AddRefs(selNode),
&selOffset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(selNode, NS_ERROR_NULL_POINTER);
arrayOfNodes.AppendElement(*selNode);
}
// remember root node
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIDOMElement> rootElem = do_QueryInterface(mHTMLEditor->GetRoot());
NS_ENSURE_TRUE(rootElem, NS_ERROR_NULL_POINTER);
// 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(GetAsDOMNode(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 {
nsCOMPtr<nsIDOMNode> node, tmp = GetAsDOMNode(curNode);
tmp->GetParentNode(getter_AddRefs(node));
while (node) {
if (node == rootElem) {
format.Truncate(0);
break;
} else if (HTMLEditUtils::IsFormatNode(node)) {
GetFormatString(node, format);
break;
}
// else keep looking up
tmp = node;
tmp->GetParentNode(getter_AddRefs(node));
}
}
// 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(nsIDOMNode* aNode,
nsAString& outFormat)
{
NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
if (HTMLEditUtils::IsFormatNode(aNode)) {
nsCOMPtr<nsIAtom> atom = EditorBase::GetTag(aNode);
atom->ToString(outFormat);
} else {
outFormat.Truncate();
}
return NS_OK;
}
void
HTMLEditRules::WillInsert(Selection& aSelection,
bool* aCancel)
{
MOZ_ASSERT(aCancel);
TextEditRules::WillInsert(aSelection, aCancel);
NS_ENSURE_TRUE_VOID(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// 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 (!aSelection.Collapsed()) {
return;
}
// If we are after a mozBR in the same block, then move selection to be
// before it
NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) &&
aSelection.GetRangeAt(0)->GetStartParent());
OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent();
int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset();
// Get prior node
nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorHTMLNode(selNode,
selOffset);
if (priorNode && TextEditUtils::IsMozBR(priorNode)) {
nsCOMPtr<Element> block1 = htmlEditor->GetBlock(selNode);
nsCOMPtr<Element> block2 = htmlEditor->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.
selNode = priorNode->GetParentNode();
selOffset = selNode->IndexOf(priorNode);
nsresult rv = aSelection.Collapse(selNode, selOffset);
NS_ENSURE_SUCCESS_VOID(rv);
}
}
if (mDidDeleteSelection &&
(mTheAction == EditAction::insertText ||
mTheAction == EditAction::insertIMEText ||
mTheAction == EditAction::deleteSelection)) {
nsresult rv = ReapplyCachedStyles();
NS_ENSURE_SUCCESS_VOID(rv);
}
// For most actions we want to clear the cached styles, but there are
// exceptions
if (!IsStyleCachePreservingAction(mTheAction)) {
ClearCachedStyles();
}
}
nsresult
HTMLEditRules::WillInsertText(EditAction aAction,
Selection* aSelection,
bool* aCancel,
bool* aHandled,
const nsAString* inString,
nsAString* outString,
int32_t aMaxLength)
{
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
if (inString->IsEmpty() && aAction != EditAction::insertIMEText) {
// HACK: this is a fix for bug 19395
// I can't outlaw all empty insertions
// because IME transaction depend on them
// There is more work to do to make the
// world safe for IME.
*aCancel = true;
*aHandled = false;
return NS_OK;
}
// 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 (!aSelection->Collapsed()) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eNoStrip);
NS_ENSURE_SUCCESS(rv, rv);
}
WillInsert(*aSelection, aCancel);
// initialize out param
// we want to ignore result of WillInsert()
*aCancel = false;
// we need to get the doc
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIDocument> doc = mHTMLEditor->GetDocument();
NS_ENSURE_STATE(doc);
// for every property that is set, insert a new inline style node
nsresult rv = CreateStyleForInsertText(*aSelection, *doc);
NS_ENSURE_SUCCESS(rv, rv);
// get the (collapsed) selection location
NS_ENSURE_STATE(mHTMLEditor);
NS_ENSURE_STATE(aSelection->GetRangeAt(0));
nsCOMPtr<nsINode> selNode = aSelection->GetRangeAt(0)->GetStartParent();
int32_t selOffset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_STATE(selNode);
// dont put text in places that can't have it
NS_ENSURE_STATE(mHTMLEditor);
if (!EditorBase::IsTextNode(selNode) &&
(!mHTMLEditor || !mHTMLEditor->CanContainTag(*selNode,
*nsGkAtoms::textTagName))) {
return NS_ERROR_FAILURE;
}
if (aAction == EditAction::insertIMEText) {
// Right now the WSRunObject code bails on empty strings, but IME needs
// the InsertTextImpl() call to still happen since empty strings are meaningful there.
NS_ENSURE_STATE(mHTMLEditor);
// If there is one or more IME selections, its minimum offset should be
// the insertion point.
int32_t IMESelectionOffset =
mHTMLEditor->GetIMESelectionStartOffsetIn(selNode);
if (IMESelectionOffset >= 0) {
selOffset = IMESelectionOffset;
}
if (inString->IsEmpty()) {
rv = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode),
&selOffset, doc);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
WSRunObject wsObj(mHTMLEditor, selNode, selOffset);
rv = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
// aAction == kInsertText
else {
// find where we are
nsCOMPtr<nsINode> curNode = selNode;
int32_t curOffset = selOffset;
// is our text going to be PREformatted?
// We remember this so that we know how to handle tabs.
bool isPRE;
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->IsPreformatted(GetAsDOMNode(selNode), &isPRE);
NS_ENSURE_SUCCESS(rv, rv);
// 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 spaz my selection in subtransactions
NS_ENSURE_STATE(mHTMLEditor);
AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
nsAutoString tString(*inString);
const char16_t *unicodeBuf = tString.get();
int32_t pos = 0;
NS_NAMED_LITERAL_STRING(newlineStr, LFSTR);
// 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)) {
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> br =
mHTMLEditor->CreateBRImpl(address_of(curNode), &curOffset,
nsIEditor::eNone);
NS_ENSURE_STATE(br);
pos++;
} else {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->InsertTextImpl(subStr, address_of(curNode),
&curOffset, doc);
NS_ENSURE_SUCCESS(rv, rv);
}
}
} 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);
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject wsObj(mHTMLEditor, curNode, curOffset);
// is it a tab?
if (subStr.Equals(tabStr)) {
rv =
wsObj.InsertText(spacesStr, address_of(curNode), &curOffset, doc);
NS_ENSURE_SUCCESS(rv, rv);
pos++;
}
// is it a return?
else if (subStr.Equals(newlineStr)) {
nsCOMPtr<Element> br = wsObj.InsertBreak(address_of(curNode),
&curOffset,
nsIEditor::eNone);
NS_ENSURE_TRUE(br, NS_ERROR_FAILURE);
pos++;
} else {
rv = wsObj.InsertText(subStr, address_of(curNode), &curOffset, doc);
NS_ENSURE_SUCCESS(rv, rv);
}
}
}
aSelection->SetInterlinePosition(false);
if (curNode) aSelection->Collapse(curNode, curOffset);
// manually update the doc changed range so that AfterEdit will clean up
// the correct portion of the document.
if (!mDocChangeRange) {
mDocChangeRange = new nsRange(selNode);
}
rv = mDocChangeRange->SetStart(selNode, selOffset);
NS_ENSURE_SUCCESS(rv, rv);
if (curNode) {
rv = mDocChangeRange->SetEnd(curNode, curOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = mDocChangeRange->SetEnd(selNode, selOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
return NS_OK;
}
nsresult
HTMLEditRules::WillLoadHTML(Selection* aSelection,
bool* aCancel)
{
NS_ENSURE_TRUE(aSelection && aCancel, NS_ERROR_NULL_POINTER);
*aCancel = false;
// Delete mBogusNode if it exists. If we really need one,
// it will be added during post-processing in AfterEditInner().
if (mBogusNode) {
if (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_UNEXPECTED;
}
mHTMLEditor->DeleteNode(mBogusNode);
mBogusNode = nullptr;
}
return NS_OK;
}
nsresult
HTMLEditRules::WillInsertBreak(Selection& aSelection,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aCancel && aHandled);
*aCancel = false;
*aHandled = false;
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// If the selection isn't collapsed, delete it.
if (!aSelection.Collapsed()) {
nsresult rv =
htmlEditor->DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip);
NS_ENSURE_SUCCESS(rv, rv);
}
WillInsert(aSelection, aCancel);
// Initialize out param. We want to ignore result of WillInsert().
*aCancel = false;
// Split any mailcites in the way. Should we abort this if we encounter
// table cell boundaries?
if (IsMailEditor()) {
nsresult rv = SplitMailCites(&aSelection, aHandled);
NS_ENSURE_SUCCESS(rv, rv);
if (*aHandled) {
return NS_OK;
}
}
// Smart splitting rules
NS_ENSURE_TRUE(aSelection.GetRangeAt(0) &&
aSelection.GetRangeAt(0)->GetStartParent(),
NS_ERROR_FAILURE);
OwningNonNull<nsINode> node = *aSelection.GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
// Do nothing if the node is read-only
if (!htmlEditor->IsModifiableNode(node)) {
*aCancel = true;
return NS_OK;
}
// Identify the block
nsCOMPtr<Element> blockParent = htmlEditor->GetBlock(node);
NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
// If the active editing host is an inline element, or if the active editing
// host is the block parent itself, just append a br.
nsCOMPtr<Element> host = htmlEditor->GetActiveEditingHost();
if (!EditorUtils::IsDescendantOf(blockParent, host)) {
nsresult rv = StandardBreakImpl(node, offset, aSelection);
NS_ENSURE_SUCCESS(rv, rv);
*aHandled = true;
return NS_OK;
}
// 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.)
bool isEmpty;
IsEmptyBlock(*blockParent, &isEmpty);
if (isEmpty) {
nsCOMPtr<Element> br = htmlEditor->CreateBR(blockParent,
blockParent->Length());
NS_ENSURE_STATE(br);
}
nsCOMPtr<Element> listItem = IsInListItem(blockParent);
if (listItem && listItem != host) {
ReturnInListItem(aSelection, *listItem, node, offset);
*aHandled = true;
return NS_OK;
} else if (HTMLEditUtils::IsHeader(*blockParent)) {
// Headers: close (or split) header
ReturnInHeader(aSelection, *blockParent, node, offset);
*aHandled = true;
return NS_OK;
} else if (blockParent->IsHTMLElement(nsGkAtoms::p)) {
// Paragraphs: special rules to look for <br>s
nsresult rv =
ReturnInParagraph(&aSelection, GetAsDOMNode(blockParent),
GetAsDOMNode(node), offset, aCancel, aHandled);
NS_ENSURE_SUCCESS(rv, rv);
// Fall through, we may not have handled it in ReturnInParagraph()
}
// If not already handled then do the standard thing
if (!(*aHandled)) {
*aHandled = true;
return StandardBreakImpl(node, offset, aSelection);
}
return NS_OK;
}
nsresult
HTMLEditRules::StandardBreakImpl(nsINode& aNode,
int32_t aOffset,
Selection& aSelection)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
nsCOMPtr<Element> brNode;
bool bAfterBlock = false;
bool bBeforeBlock = false;
nsCOMPtr<nsINode> node = &aNode;
if (IsPlaintextEditor()) {
brNode = htmlEditor->CreateBR(node, aOffset);
NS_ENSURE_STATE(brNode);
} else {
WSRunObject wsObj(htmlEditor, node, aOffset);
int32_t visOffset = 0;
WSType wsType;
nsCOMPtr<nsINode> visNode;
wsObj.PriorVisibleNode(node, aOffset, address_of(visNode),
&visOffset, &wsType);
if (wsType & WSType::block) {
bAfterBlock = true;
}
wsObj.NextVisibleNode(node, aOffset, address_of(visNode),
&visOffset, &wsType);
if (wsType & WSType::block) {
bBeforeBlock = true;
}
nsCOMPtr<nsIDOMNode> linkDOMNode;
if (htmlEditor->IsInLink(GetAsDOMNode(node), address_of(linkDOMNode))) {
// Split the link
nsCOMPtr<Element> linkNode = do_QueryInterface(linkDOMNode);
NS_ENSURE_STATE(linkNode || !linkDOMNode);
nsCOMPtr<nsINode> linkParent = linkNode->GetParentNode();
aOffset = htmlEditor->SplitNodeDeep(*linkNode, *node->AsContent(),
aOffset,
HTMLEditor::EmptyContainers::no);
NS_ENSURE_STATE(aOffset != -1);
node = linkParent;
}
brNode = wsObj.InsertBreak(address_of(node), &aOffset, nsIEditor::eNone);
NS_ENSURE_TRUE(brNode, NS_ERROR_FAILURE);
}
node = brNode->GetParentNode();
NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
int32_t offset = node->IndexOf(brNode);
if (bAfterBlock && bBeforeBlock) {
// 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.
aSelection.SetInterlinePosition(true);
nsresult rv = aSelection.Collapse(node, offset);
NS_ENSURE_SUCCESS(rv, rv);
} else {
WSRunObject wsObj(htmlEditor, node, offset + 1);
nsCOMPtr<nsINode> secondBR;
int32_t visOffset = 0;
WSType wsType;
wsObj.NextVisibleNode(node, offset + 1, address_of(secondBR),
&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.
nsCOMPtr<nsINode> brParent = secondBR->GetParentNode();
int32_t brOffset = brParent ? brParent->IndexOf(secondBR) : -1;
if (brParent != node || brOffset != offset + 1) {
nsresult rv =
htmlEditor->MoveNode(secondBR->AsContent(), node, offset + 1);
NS_ENSURE_SUCCESS(rv, 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.
nsCOMPtr<nsIContent> siblingNode = brNode->GetNextSibling();
if (siblingNode && IsBlockNode(*siblingNode)) {
aSelection.SetInterlinePosition(false);
} else {
aSelection.SetInterlinePosition(true);
}
nsresult rv = aSelection.Collapse(node, offset + 1);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult
HTMLEditRules::DidInsertBreak(Selection* aSelection,
nsresult aResult)
{
return NS_OK;
}
nsresult
HTMLEditRules::SplitMailCites(Selection* aSelection,
bool* aHandled)
{
NS_ENSURE_TRUE(aSelection && aHandled, NS_ERROR_NULL_POINTER);
nsCOMPtr<nsIContent> leftCite, rightCite;
nsCOMPtr<nsINode> selNode;
nsCOMPtr<Element> citeNode;
int32_t selOffset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(aSelection, getter_AddRefs(selNode),
&selOffset);
NS_ENSURE_SUCCESS(rv, rv);
citeNode = GetTopEnclosingMailCite(*selNode);
if (citeNode) {
// 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.
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject wsObj(mHTMLEditor, selNode, selOffset);
nsCOMPtr<nsINode> visNode;
int32_t visOffset=0;
WSType wsType;
wsObj.NextVisibleNode(selNode, selOffset, address_of(visNode),
&visOffset, &wsType);
if (wsType == WSType::br) {
// ok, we are just before a break. is it inside the mailquote?
if (visNode != citeNode && citeNode->Contains(visNode)) {
// it is. so lets reset our selection to be just after it.
NS_ENSURE_STATE(mHTMLEditor);
selNode = mHTMLEditor->GetNodeLocation(visNode, &selOffset);
++selOffset;
}
}
NS_ENSURE_STATE(mHTMLEditor);
NS_ENSURE_STATE(selNode->IsContent());
int32_t newOffset = mHTMLEditor->SplitNodeDeep(*citeNode,
*selNode->AsContent(), selOffset, HTMLEditor::EmptyContainers::no,
getter_AddRefs(leftCite), getter_AddRefs(rightCite));
NS_ENSURE_STATE(newOffset != -1);
// Add an invisible <br> to the end of the left part if it was a <span> of
// style="display: block". This is important, since when serialising the
// cite to plain text, the span which caused the visual break is discarded.
// So the added <br> will guarantee that the serialiser will insert a
// break where the user saw one.
if (leftCite &&
leftCite->IsHTMLElement(nsGkAtoms::span) &&
leftCite->GetPrimaryFrame()->IsFrameOfType(nsIFrame::eBlockFrame)) {
nsCOMPtr<nsINode> lastChild = leftCite->GetLastChild();
if (lastChild && !lastChild->IsHTMLElement(nsGkAtoms::br)) {
// We ignore the result here.
nsCOMPtr<Element> invisBR =
mHTMLEditor->CreateBR(leftCite, leftCite->Length());
}
}
selNode = citeNode->GetParentNode();
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(selNode, newOffset);
NS_ENSURE_STATE(brNode);
// want selection before the break, and on same line
aSelection->SetInterlinePosition(true);
rv = aSelection->Collapse(selNode, newOffset);
NS_ENSURE_SUCCESS(rv, rv);
// 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)) {
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject wsObj(mHTMLEditor, selNode, newOffset);
nsCOMPtr<nsINode> visNode;
int32_t visOffset=0;
WSType wsType;
wsObj.PriorVisibleNode(selNode, newOffset, address_of(visNode),
&visOffset, &wsType);
if (wsType == WSType::normalWS || wsType == WSType::text ||
wsType == WSType::special) {
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject wsObjAfterBR(mHTMLEditor, selNode, newOffset+1);
wsObjAfterBR.NextVisibleNode(selNode, newOffset + 1,
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) {
NS_ENSURE_STATE(mHTMLEditor);
brNode = mHTMLEditor->CreateBR(selNode, newOffset);
NS_ENSURE_STATE(brNode);
}
}
}
// delete any empty cites
bool bEmptyCite = false;
if (leftCite) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->IsEmptyNode(leftCite, &bEmptyCite, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (bEmptyCite) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(leftCite);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
if (rightCite) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->IsEmptyNode(rightCite, &bEmptyCite, true, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (bEmptyCite) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(rightCite);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
*aHandled = true;
}
return NS_OK;
}
nsresult
HTMLEditRules::WillDeleteSelection(Selection* aSelection,
nsIEditor::EDirection aAction,
nsIEditor::EStripWrappers aStripWrappers,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aStripWrappers == nsIEditor::eStrip ||
aStripWrappers == nsIEditor::eNoStrip);
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
// 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.
nsCOMPtr<nsIDOMElement> cell;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetFirstSelectedCell(nullptr, getter_AddRefs(cell));
if (NS_SUCCEEDED(rv) && cell) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteTableCellContents();
*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. TryToJoinBlocks() 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 = aSelection->Collapsed();
bool join = false;
bool origCollapsed = bCollapsed;
nsCOMPtr<nsINode> selNode;
int32_t selOffset;
NS_ENSURE_STATE(aSelection->GetRangeAt(0));
nsCOMPtr<nsINode> startNode = aSelection->GetRangeAt(0)->GetStartParent();
int32_t startOffset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
if (bCollapsed) {
// If we are inside an empty block, delete it.
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> host = mHTMLEditor->GetActiveEditingHost();
NS_ENSURE_TRUE(host, NS_ERROR_FAILURE);
rv = CheckForEmptyBlock(startNode, host, aSelection, aAction, aHandled);
NS_ENSURE_SUCCESS(rv, rv);
if (*aHandled) {
return NS_OK;
}
// Test for distance between caret and text that will be deleted
rv = CheckBidiLevelForDeletion(aSelection, GetAsDOMNode(startNode),
startOffset, aAction, aCancel);
NS_ENSURE_SUCCESS(rv, rv);
if (*aCancel) {
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->ExtendSelectionForDelete(aSelection, &aAction);
NS_ENSURE_SUCCESS(rv, rv);
// We should delete nothing.
if (aAction == nsIEditor::eNone) {
return NS_OK;
}
// ExtendSelectionForDelete() may have changed the selection, update it
NS_ENSURE_STATE(aSelection->GetRangeAt(0));
startNode = aSelection->GetRangeAt(0)->GetStartParent();
startOffset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
bCollapsed = aSelection->Collapsed();
}
if (bCollapsed) {
// What's in the direction we are deleting?
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject wsObj(mHTMLEditor, startNode, startOffset);
nsCOMPtr<nsINode> visNode;
int32_t visOffset;
WSType wsType;
// Find next visible node
if (aAction == nsIEditor::eNext) {
wsObj.NextVisibleNode(startNode, startOffset, address_of(visNode),
&visOffset, &wsType);
} else {
wsObj.PriorVisibleNode(startNode, startOffset, address_of(visNode),
&visOffset, &wsType);
}
if (!visNode) {
// Can't find anything to delete!
*aCancel = true;
// XXX This is the result of mHTMLEditor->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(NS_FAILED(rv))) {
return rv;
}
} else {
rv = wsObj.DeleteWSBackward();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return InsertBRIfNeeded(aSelection);
}
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 = aSelection->GetRangeAt(0);
NS_ENSURE_STATE(range);
NS_ASSERTION(range->GetStartParent() == visNode,
"selection start not in visNode");
NS_ASSERTION(range->GetEndParent() == visNode,
"selection end not in visNode");
so = range->StartOffset();
eo = range->EndOffset();
}
NS_ENSURE_STATE(mHTMLEditor);
rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(visNode),
&so, address_of(visNode), &eo);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
*aHandled = true;
rv = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo),
DeprecatedAbs(eo - so));
NS_ENSURE_SUCCESS(rv, 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.
DeleteNodeIfCollapsedText(nodeAsText);
rv = InsertBRIfNeeded(aSelection);
NS_ENSURE_SUCCESS(rv, 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) &&
(!mHTMLEditor || !mHTMLEditor->IsVisBreak(visNode))) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(visNode);
NS_ENSURE_SUCCESS(rv, rv);
return WillDeleteSelection(aSelection, aAction, aStripWrappers,
aCancel, aHandled);
}
// 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->IndexOf(visNode) : -1;
bool interLineIsRight;
rv = aSelection->GetInterlinePosition(&interLineIsRight);
NS_ENSURE_SUCCESS(rv, rv);
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;
aSelection->Collapse(selNode, selOffset);
aSelection->SetInterlinePosition(false);
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(startNode, startOffset, address_of(otherNode),
&otherOffset, &otherWSType);
if (otherWSType == WSType::br) {
// Delete the <br>
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIContent> otherContent(do_QueryInterface(otherNode));
rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, otherContent);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(otherNode);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
// Else continue with normal delete code
}
// Found break or image, or hr.
NS_ENSURE_STATE(mHTMLEditor);
NS_ENSURE_STATE(visNode->IsContent());
rv = WSRunObject::PrepareToDeleteNode(mHTMLEditor, visNode->AsContent());
NS_ENSURE_SUCCESS(rv, rv);
// Remember sibling to visnode, if any
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIContent> sibling = mHTMLEditor->GetPriorHTMLSibling(visNode);
// Delete the node, and join like nodes if appropriate
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(visNode);
NS_ENSURE_SUCCESS(rv, 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) {
NS_ENSURE_STATE(mHTMLEditor);
stepbrother = mHTMLEditor->GetNextHTMLSibling(sibling);
}
// Are they both text nodes? If so, join them!
if (startNode == stepbrother && startNode->GetAsText() &&
sibling->GetAsText()) {
EditorDOMPoint pt = JoinNodesSmart(*sibling, *startNode->AsContent());
NS_ENSURE_STATE(pt.node);
// Fix up selection
rv = aSelection->Collapse(pt.node, pt.offset);
NS_ENSURE_SUCCESS(rv, rv);
}
rv = InsertBRIfNeeded(aSelection);
NS_ENSURE_SUCCESS(rv, 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(startNode, startOffset, address_of(otherNode),
&otherOffset, &otherWSType);
} else {
wsObj.NextVisibleNode(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) {
NS_ENSURE_STATE(mHTMLEditor);
leafNode = mHTMLEditor->GetLastEditableLeaf(*visNode);
leftNode = leafNode;
rightNode = startNode;
} else {
NS_ENSURE_STATE(mHTMLEditor);
leafNode = mHTMLEditor->GetFirstEditableLeaf(*visNode);
leftNode = startNode;
rightNode = leafNode;
}
if (otherNode->IsHTMLElement(nsGkAtoms::br)) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(otherNode);
NS_ENSURE_SUCCESS(rv, 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.
NS_ENSURE_STATE(leafNode);
EditorDOMPoint newSel = GetGoodSelPointForNode(*leafNode, aAction);
NS_ENSURE_STATE(newSel.node);
aSelection->Collapse(newSel.node, newSel.offset);
return NS_OK;
}
// Else we are joining content to block
nsCOMPtr<nsINode> selPointNode = startNode;
int32_t selPointOffset = startOffset;
{
NS_ENSURE_STATE(mHTMLEditor);
AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
address_of(selPointNode), &selPointOffset);
NS_ENSURE_STATE(leftNode && leftNode->IsContent() &&
rightNode && rightNode->IsContent());
EditActionResult ret =
TryToJoinBlocks(*leftNode->AsContent(), *rightNode->AsContent());
*aHandled |= ret.Handled();
*aCancel |= ret.Canceled();
if (NS_WARN_IF(ret.Failed())) {
return ret.Rv();
}
}
// If TryToJoinBlocks() 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;
aSelection->Collapse(leafNode, offset);
return WillDeleteSelection(aSelection, aAction, aStripWrappers,
aCancel, aHandled);
}
// Otherwise, we must have deleted the selection as user expected.
aSelection->Collapse(selPointNode, selPointOffset);
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) {
NS_ENSURE_STATE(mHTMLEditor);
leftNode = mHTMLEditor->GetPriorHTMLNode(visNode);
rightNode = startNode;
} else {
NS_ENSURE_STATE(mHTMLEditor);
rightNode = mHTMLEditor->GetNextHTMLNode(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;
{
NS_ENSURE_STATE(mHTMLEditor);
AutoTrackDOMPoint tracker(mHTMLEditor->mRangeUpdater,
address_of(selPointNode), &selPointOffset);
NS_ENSURE_STATE(leftNode->IsContent() && rightNode->IsContent());
EditActionResult ret =
TryToJoinBlocks(*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();
}
}
aSelection->Collapse(selPointNode, selPointOffset);
return NS_OK;
}
}
// Else we have a non-collapsed selection. First adjust the selection.
rv = ExpandSelectionForDeletion(*aSelection);
NS_ENSURE_SUCCESS(rv, rv);
// Remember that we did a ranged delete for the benefit of AfterEditInner().
mDidRangedDelete = true;
// Refresh start and end points
NS_ENSURE_STATE(aSelection->GetRangeAt(0));
startNode = aSelection->GetRangeAt(0)->GetStartParent();
startOffset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
nsCOMPtr<nsINode> endNode = aSelection->GetRangeAt(0)->GetEndParent();
int32_t endOffset = aSelection->GetRangeAt(0)->EndOffset();
NS_ENSURE_TRUE(endNode, NS_ERROR_FAILURE);
// Figure out if the endpoints are in nodes that can be merged. Adjust
// surrounding whitespace in preparation to delete selection.
if (!IsPlaintextEditor()) {
NS_ENSURE_STATE(mHTMLEditor);
AutoTransactionsConserveSelection dontSpazMySelection(mHTMLEditor);
rv = WSRunObject::PrepareToDeleteRange(mHTMLEditor,
address_of(startNode), &startOffset,
address_of(endNode), &endOffset);
NS_ENSURE_SUCCESS(rv, rv);
}
{
// Track location of where we are deleting
NS_ENSURE_STATE(mHTMLEditor);
AutoTrackDOMPoint startTracker(mHTMLEditor->mRangeUpdater,
address_of(startNode), &startOffset);
AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
address_of(endNode), &endOffset);
// We are handling all ranged deletions directly now.
*aHandled = true;
if (endNode == startNode) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
NS_ENSURE_SUCCESS(rv, 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
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> leftParent = mHTMLEditor->GetBlock(*startNode);
nsCOMPtr<Element> rightParent = mHTMLEditor->GetBlock(*endNode);
// Are endpoint block parents the same? Use default deletion
if (leftParent && leftParent == rightParent) {
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
} else {
// Deleting across blocks. Are the blocks of same type?
NS_ENSURE_STATE(leftParent && rightParent);
// Are the blocks siblings?
nsCOMPtr<nsINode> leftBlockParent = leftParent->GetParentNode();
nsCOMPtr<nsINode> rightBlockParent = rightParent->GetParentNode();
// MOOSE: this could conceivably screw up a table.. fix me.
NS_ENSURE_STATE(mHTMLEditor);
if (leftBlockParent == rightBlockParent &&
mHTMLEditor->NodesSameType(GetAsDOMNode(leftParent),
GetAsDOMNode(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
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteSelectionImpl(aAction, aStripWrappers);
NS_ENSURE_SUCCESS(rv, rv);
// Join blocks
NS_ENSURE_STATE(mHTMLEditor);
EditorDOMPoint pt =
mHTMLEditor->JoinNodeDeep(*leftParent, *rightParent);
NS_ENSURE_STATE(pt.node);
// Fix up selection
rv = aSelection->Collapse(pt.node, pt.offset);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
// Else blocks not same type, or not siblings. Delete everything
// except table elements.
join = true;
uint32_t rangeCount = aSelection->RangeCount();
for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
OwningNonNull<nsRange> range = *aSelection->GetRangeAt(rangeIdx);
// Build a list of nodes in the range
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
TrivialFunctor functor;
DOMSubtreeIterator iter;
nsresult rv = iter.Init(*range);
NS_ENSURE_SUCCESS(rv, 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++) {
nsCOMPtr<nsINode> somenode = do_QueryInterface(arrayOfNodes[0]);
NS_ENSURE_STATE(somenode);
DeleteNonTableElements(somenode);
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 (!somenode->IsContent()) {
join = false;
continue;
}
nsCOMPtr<nsIContent> content = somenode->AsContent();
if (content->NodeType() == nsIDOMNode::TEXT_NODE) {
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->IsVisTextNode(content, &join, true);
} else {
NS_ENSURE_STATE(mHTMLEditor);
join = content->IsHTMLElement(nsGkAtoms::br) &&
!mHTMLEditor->IsVisBreak(somenode);
}
}
}
}
// 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<nsGenericDOMDataNode> dataNode =
*static_cast<nsGenericDOMDataNode*>(startNode.get());
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteText(dataNode, startOffset,
startNode->Length() - startOffset);
NS_ENSURE_SUCCESS(rv, rv);
}
if (endNode->GetAsText() && endOffset) {
// Delete to first character
NS_ENSURE_STATE(mHTMLEditor);
OwningNonNull<nsGenericDOMDataNode> dataNode =
*static_cast<nsGenericDOMDataNode*>(endNode.get());
rv = mHTMLEditor->DeleteText(dataNode, 0, endOffset);
NS_ENSURE_SUCCESS(rv, rv);
}
if (join) {
EditActionResult ret = TryToJoinBlocks(*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(mHTMLEditor->mRangeUpdater,
address_of(startNode), &startOffset);
AutoTrackDOMPoint endTracker(mHTMLEditor->mRangeUpdater,
address_of(endNode), &endOffset);
DeleteNodeIfCollapsedText(*startNode);
DeleteNodeIfCollapsedText(*endNode);
}
// 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 = aSelection->Collapse(endNode, endOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = aSelection->Collapse(startNode, startOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
/**
* If aNode is a text node that contains only collapsed whitespace, delete it.
* It doesn't serve any useful purpose, and we don't want it to confuse code
* that doesn't correctly skip over it.
*
* If deleting the node fails (like if it's not editable), the caller should
* proceed as usual, so don't return any errors.
*/
void
HTMLEditRules::DeleteNodeIfCollapsedText(nsINode& aNode)
{
if (!aNode.GetAsText()) {
return;
}
bool empty;
nsresult rv = mHTMLEditor->IsVisTextNode(aNode.AsContent(), &empty, false);
NS_ENSURE_SUCCESS_VOID(rv);
if (empty) {
mHTMLEditor->DeleteNode(&aNode);
}
}
/**
* InsertBRIfNeeded() determines if a br is needed for current selection to not
* be spastic. If so, it inserts one. Callers responsibility to only call
* with collapsed selection.
*
* @param aSelection The collapsed selection.
*/
nsresult
HTMLEditRules::InsertBRIfNeeded(Selection* aSelection)
{
NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
// get selection
nsCOMPtr<nsINode> node;
int32_t offset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(aSelection,
getter_AddRefs(node), &offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(node, NS_ERROR_FAILURE);
// inline elements don't need any br
if (!IsBlockNode(*node)) {
return NS_OK;
}
// examine selection
NS_ENSURE_STATE(mHTMLEditor);
WSRunObject wsObj(mHTMLEditor, node, offset);
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
NS_ENSURE_STATE(mHTMLEditor);
if (mHTMLEditor->CanContainTag(*node, *nsGkAtoms::br)) {
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> br =
mHTMLEditor->CreateBR(node, offset, nsIEditor::ePrevious);
return br ? NS_OK : NS_ERROR_FAILURE;
}
}
return NS_OK;
}
/**
* GetGoodSelPointForNode() finds where at a node you would want to set the
* selection if you were trying to have a caret next to it. Always returns a
* valid value (unless mHTMLEditor has gone away).
*
* @param aNode The node
* @param aAction Which edge to find:
* eNext/eNextWord/eToEndOfLine indicates beginning,
* ePrevious/PreviousWord/eToBeginningOfLine ending.
*/
EditorDOMPoint
HTMLEditRules::GetGoodSelPointForNode(nsINode& aNode,
nsIEditor::EDirection aAction)
{
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);
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
if (aNode.GetAsText() || mHTMLEditor->IsContainer(&aNode) ||
NS_WARN_IF(!aNode.GetParentNode())) {
return EditorDOMPoint(&aNode, isPreviousAction ? aNode.Length() : 0);
}
EditorDOMPoint ret;
ret.node = aNode.GetParentNode();
ret.offset = ret.node ? ret.node->IndexOf(&aNode) : -1;
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
if ((!aNode.IsHTMLElement(nsGkAtoms::br) ||
mHTMLEditor->IsVisBreak(&aNode)) && isPreviousAction) {
ret.offset++;
}
return ret;
}
EditActionResult
HTMLEditRules::TryToJoinBlocks(nsIContent& aLeftNode,
nsIContent& aRightNode)
{
if (NS_WARN_IF(!mHTMLEditor)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
nsCOMPtr<Element> leftBlock = htmlEditor->GetBlock(aLeftNode);
nsCOMPtr<Element> rightBlock = htmlEditor->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 = htmlEditor->GetBlockNodeParent(leftBlock);
if (NS_WARN_IF(!leftBlock)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
}
if (rightBlock->IsHTMLElement(nsGkAtoms::hr)) {
rightBlock = htmlEditor->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;
nsIAtom* existingList = nsGkAtoms::_empty;
int32_t offset;
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, &offset) &&
!EditorUtils::IsDescendantOf(rightList, leftBlock, &offset)) {
// 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.
leftBlock = leftList;
rightBlock = rightList;
mergeLists = true;
existingList = leftList->NodeInfo()->NameAtom();
}
}
AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor);
int32_t rightOffset = 0;
int32_t leftOffset = -1;
// offset below is where you find yourself in rightBlock when you traverse
// upwards from leftBlock
if (EditorUtils::IsDescendantOf(leftBlock, rightBlock, &rightOffset)) {
// Tricky case. Left block is inside right block. Do ws adjustment. This
// just destroys non-visible ws at boundaries we will be joining.
rightOffset++;
nsresult rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
WSRunObject::kBlockEnd,
leftBlock);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
{
// We can't just track rightBlock because it's an Element.
nsCOMPtr<nsINode> trackingRightBlock(rightBlock);
AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater,
address_of(trackingRightBlock), &rightOffset);
rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
WSRunObject::kAfterBlock,
rightBlock, rightOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
if (trackingRightBlock->IsElement()) {
rightBlock = trackingRightBlock->AsElement();
} else {
if (NS_WARN_IF(!trackingRightBlock->GetParentElement())) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
rightBlock = trackingRightBlock->GetParentElement();
}
}
// Do br adjustment.
nsCOMPtr<Element> brNode =
CheckForInvisibleBR(*leftBlock, BRLocation::blockEnd);
EditActionResult ret(NS_OK);
if (mergeLists) {
// The idea here is to take all children in rightList that are past
// offset, and pull them into leftlist.
for (nsCOMPtr<nsIContent> child = rightList->GetChildAt(offset);
child; child = rightList->GetChildAt(rightOffset)) {
rv = htmlEditor->MoveNode(child, leftList, -1);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
}
// XXX Should this set to true only when above for loop moves the node?
ret.MarkAsHandled();
} else {
// XXX Why do we ignore the result of MoveBlock()?
EditActionResult retMoveBlock =
MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset);
if (retMoveBlock.Handled()) {
ret.MarkAsHandled();
}
}
if (brNode && NS_SUCCEEDED(htmlEditor->DeleteNode(brNode))) {
ret.MarkAsHandled();
}
return ret;
}
// Offset below is where you find yourself in leftBlock when you traverse
// upwards from rightBlock
if (EditorUtils::IsDescendantOf(rightBlock, leftBlock, &leftOffset)) {
// 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(htmlEditor,
WSRunObject::kBlockStart,
rightBlock);
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.
nsCOMPtr<nsINode> trackingLeftBlock(leftBlock);
AutoTrackDOMPoint tracker(htmlEditor->mRangeUpdater,
address_of(trackingLeftBlock), &leftOffset);
rv = WSRunObject::ScrubBlockBoundary(htmlEditor,
WSRunObject::kBeforeBlock,
leftBlock, leftOffset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
if (trackingLeftBlock->IsElement()) {
leftBlock = trackingLeftBlock->AsElement();
} else {
if (NS_WARN_IF(!trackingLeftBlock->GetParentElement())) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
leftBlock = trackingLeftBlock->GetParentElement();
}
}
// Do br adjustment.
nsCOMPtr<Element> brNode =
CheckForInvisibleBR(*leftBlock, BRLocation::beforeBlock, leftOffset);
EditActionResult ret(NS_OK);
if (mergeLists) {
// XXX Why do we ignore the result of MoveContents()?
EditActionResult retMoveContents =
MoveContents(*rightList, *leftList, &leftOffset);
if (retMoveContents.Handled()) {
ret.MarkAsHandled();
}
} 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.
int32_t previousContentOffset;
nsCOMPtr<nsINode> previousContentParent;
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.
previousContentParent = leftBlock;
previousContentOffset = leftOffset;
} 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.
previousContentParent = aLeftNode.GetParentNode();
previousContentOffset = previousContentParent ?
previousContentParent->IndexOf(&aLeftNode) : -1;
// We want to move our content just after the previous visible node.
previousContentOffset++;
}
// Because we don't want the moving content to receive the style of the
// previous content, we split the previous content's style.
nsCOMPtr<Element> editorRoot = htmlEditor->GetEditorRoot();
if (!editorRoot || &aLeftNode != editorRoot) {
nsCOMPtr<nsIContent> splittedPreviousContent;
rv = htmlEditor->SplitStyleAbovePoint(
address_of(previousContentParent),
&previousContentOffset,
nullptr, nullptr, nullptr,
getter_AddRefs(splittedPreviousContent));
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditActionIgnored(rv);
}
if (splittedPreviousContent) {
previousContentParent = splittedPreviousContent->GetParentNode();
previousContentOffset = previousContentParent ?
previousContentParent->IndexOf(splittedPreviousContent) : -1;
}
}
if (NS_WARN_IF(!previousContentParent)) {
return EditActionIgnored(NS_ERROR_NULL_POINTER);
}
ret |= MoveBlock(*previousContentParent->AsElement(), *rightBlock,
previousContentOffset, rightOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
}
if (brNode && NS_SUCCEEDED(htmlEditor->DeleteNode(brNode))) {
ret.MarkAsHandled();
}
return ret;
}
// 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(htmlEditor, leftBlock, rightBlock);
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 = JoinNodesSmart(*leftBlock, *rightBlock);
if (pt.node && mergeLists) {
nsCOMPtr<Element> newBlock;
ConvertListType(rightBlock, getter_AddRefs(newBlock),
existingList, nsGkAtoms::li);
}
ret.MarkAsHandled();
} else {
// Nodes are dissimilar types.
ret |= MoveBlock(*leftBlock, *rightBlock, leftOffset, rightOffset);
if (NS_WARN_IF(ret.Failed())) {
return ret;
}
}
if (brNode) {
rv = htmlEditor->DeleteNode(brNode);
// XXX In other top level if blocks, the result of DeleteNode()
// 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)
{
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
// GetNodesFromPoint is the workhorse that figures out what we wnat to move.
nsresult rv = GetNodesFromPoint(EditorDOMPoint(&aRightBlock, aRightOffset),
EditAction::makeList, 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;
}
if (NS_WARN_IF(!mHTMLEditor)) {
return ret.SetResult(NS_ERROR_UNEXPECTED);
}
rv = mHTMLEditor->DeleteNode(arrayOfNodes[i]);
ret.MarkAsHandled();
} else {
// Otherwise move the content as is, checking against the DTD.
ret |=
MoveNodeSmart(*arrayOfNodes[i]->AsContent(), aLeftBlock, &aLeftOffset);
}
}
// 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(aInOutDestOffset);
if (NS_WARN_IF(!mHTMLEditor)) {
return EditActionIgnored(NS_ERROR_UNEXPECTED);
}
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// Check if this node can go into the destination node
if (htmlEditor->CanContain(aDestElement, aNode)) {
// If it can, move it there
nsresult rv =
htmlEditor->MoveNode(&aNode, &aDestElement, *aInOutDestOffset);
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 = htmlEditor->DeleteNode(&aNode);
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::DeleteNonTableElements(nsINode* aNode)
{
MOZ_ASSERT(aNode);
if (!HTMLEditUtils::IsTableElementButNotTable(aNode)) {
NS_ENSURE_STATE(mHTMLEditor);
return mHTMLEditor->DeleteNode(aNode->AsDOMNode());
}
for (int32_t i = aNode->GetChildCount() - 1; i >= 0; --i) {
nsresult rv = DeleteNonTableElements(aNode->GetChildAt(i));
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult
HTMLEditRules::DidDeleteSelection(Selection* aSelection,
nsIEditor::EDirection aDir,
nsresult aResult)
{
if (!aSelection) {
return NS_ERROR_NULL_POINTER;
}
// find where we are
nsCOMPtr<nsINode> startNode;
int32_t startOffset;
nsresult rv = EditorBase::GetStartNodeAndOffset(aSelection,
getter_AddRefs(startNode),
&startOffset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(startNode, NS_ERROR_FAILURE);
// find any enclosing mailcite
nsCOMPtr<Element> citeNode = GetTopEnclosingMailCite(*startNode);
if (citeNode) {
bool isEmpty = true, seenBR = false;
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->IsEmptyNodeImpl(citeNode, &isEmpty, true, true, false,
&seenBR);
if (isEmpty) {
int32_t offset;
nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(citeNode, &offset);
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(citeNode);
NS_ENSURE_SUCCESS(rv, rv);
if (parent && seenBR) {
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> brNode = mHTMLEditor->CreateBR(parent, offset);
NS_ENSURE_STATE(brNode);
aSelection->Collapse(parent, offset);
}
}
}
// call through to base class
return TextEditRules::DidDeleteSelection(aSelection, aDir, aResult);
}
nsresult
HTMLEditRules::WillMakeList(Selection* aSelection,
const nsAString* aListType,
bool aEntireList,
const nsAString* aBulletType,
bool* aCancel,
bool* aHandled,
const nsAString* aItemType)
{
if (!aSelection || !aListType || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
OwningNonNull<nsIAtom> listType = NS_Atomize(*aListType);
WillInsert(*aSelection, aCancel);
// initialize out param
// we want to ignore result of WillInsert()
*aCancel = false;
*aHandled = false;
// deduce what tag to use for list items
nsCOMPtr<nsIAtom> itemType;
if (aItemType) {
itemType = NS_Atomize(*aItemType);
NS_ENSURE_TRUE(itemType, NS_ERROR_OUT_OF_MEMORY);
} 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;
nsresult rv = NormalizeSelection(aSelection);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetListActionNodes(arrayOfNodes,
aEntireList ? EntireList::yes : EntireList::no);
NS_ENSURE_SUCCESS(rv, 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) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(node);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// get selection location
NS_ENSURE_STATE(aSelection->RangeCount());
nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_STATE(parent);
// make sure we can put a list here
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->CanContainTag(*parent, listType)) {
*aCancel = true;
return NS_OK;
}
rv = SplitAsNeeded(listType, parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> theList =
mHTMLEditor->CreateNode(listType, parent, offset);
NS_ENSURE_STATE(theList);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> theListItem =
mHTMLEditor->CreateNode(itemType, theList, 0);
NS_ENSURE_STATE(theListItem);
// remember our new block for postprocessing
mNewBlock = theListItem;
// put selection in new list item
*aHandled = true;
rv = aSelection->Collapse(theListItem, 0);
// Don't restore the selection
selectionRestorer.Abort();
return rv;
}
// 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();
nsCOMPtr<nsINode> curParent;
nsCOMPtr<Element> curList, prevListItem;
for (uint32_t i = 0; i < listCount; i++) {
// here's where we actually figure out what to do
nsCOMPtr<Element> newBlock;
NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
int32_t offset;
curParent = EditorBase::GetNodeLocation(curNode, &offset);
// 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 (TextEditUtils::IsBreak(curNode)) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
prevListItem = nullptr;
continue;
} else if (IsEmptyInline(curNode)) {
// if curNode is an empty inline container, delete it
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
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 RemoveContainer() on the list. ConvertListType
// first: that routine handles converting the list item types, if
// needed
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock),
listType, itemType);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->RemoveBlockContainer(*newBlock);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// replace list with new list type
rv = ConvertListType(curNode->AsElement(), getter_AddRefs(newBlock),
listType, itemType);
NS_ENSURE_SUCCESS(rv, rv);
curList = newBlock;
}
prevListItem = nullptr;
continue;
}
if (HTMLEditUtils::IsListItem(curNode)) {
NS_ENSURE_STATE(mHTMLEditor);
if (!curParent->IsHTMLElement(listType)) {
// 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)) {
NS_ENSURE_STATE(mHTMLEditor);
NS_ENSURE_STATE(curParent->IsContent());
ErrorResult rv;
nsCOMPtr<nsIContent> splitNode =
mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv);
NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
newBlock = splitNode ? splitNode->AsElement() : nullptr;
int32_t offset;
nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(curParent,
&offset);
NS_ENSURE_STATE(mHTMLEditor);
curList = mHTMLEditor->CreateNode(listType, parent, offset);
NS_ENSURE_STATE(curList);
}
// move list item to new list
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
// convert list item type if needed
NS_ENSURE_STATE(mHTMLEditor);
if (!curNode->IsHTMLElement(itemType)) {
NS_ENSURE_STATE(mHTMLEditor);
newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
itemType);
NS_ENSURE_STATE(newBlock);
}
} 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 = curParent->AsElement();
} else if (curParent != curList) {
// move list item to new list
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
NS_ENSURE_STATE(mHTMLEditor);
if (!curNode->IsHTMLElement(itemType)) {
NS_ENSURE_STATE(mHTMLEditor);
newBlock = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
itemType);
NS_ENSURE_STATE(newBlock);
}
}
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> curElement = do_QueryInterface(curNode);
if (aBulletType && !aBulletType->IsEmpty()) {
rv = mHTMLEditor->SetAttribute(curElement, nsGkAtoms::type,
*aBulletType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
rv = mHTMLEditor->RemoveAttribute(curElement, nsGkAtoms::type);
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);
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->RemoveContainer(curNode);
NS_ENSURE_SUCCESS(rv, rv);
listCount = arrayOfNodes.Length();
continue;
}
// need to make a list to put things in if we haven't already,
if (!curList) {
rv = SplitAsNeeded(listType, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curList = mHTMLEditor->CreateNode(listType, curParent, offset);
// remember our new block for postprocessing
mNewBlock = curList;
// curList is now the correct thing to put curNode in
prevListItem = nullptr;
}
// 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
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, prevListItem, -1);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// don't wrap li around a paragraph. instead replace paragraph with li
if (curNode->IsHTMLElement(nsGkAtoms::p)) {
NS_ENSURE_STATE(mHTMLEditor);
listItem = mHTMLEditor->ReplaceContainer(curNode->AsElement(),
itemType);
NS_ENSURE_STATE(listItem);
} else {
NS_ENSURE_STATE(mHTMLEditor);
listItem = mHTMLEditor->InsertContainerAbove(curNode, itemType);
NS_ENSURE_STATE(listItem);
}
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
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(listItem, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::WillRemoveList(Selection* aSelection,
bool aOrdered,
bool* aCancel,
bool* aHandled)
{
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
// initialize out param
*aCancel = false;
*aHandled = true;
nsresult rv = NormalizeSelection(aSelection);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(*aSelection, arrayOfRanges, EditAction::makeList);
// use these ranges to contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetListActionNodes(arrayOfNodes, EntireList::no);
NS_ENSURE_SUCCESS(rv, rv);
// Remove all non-editable nodes. Leave them be.
for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> testNode = arrayOfNodes[i];
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->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);
NS_ENSURE_SUCCESS(rv, 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());
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::WillMakeDefListItem(Selection* aSelection,
const nsAString *aItemType,
bool aEntireList,
bool* aCancel,
bool* aHandled)
{
// for now we let WillMakeList handle this
NS_NAMED_LITERAL_STRING(listType, "dl");
return WillMakeList(aSelection, &listType, aEntireList, nullptr, aCancel, aHandled, aItemType);
}
nsresult
HTMLEditRules::WillMakeBasicBlock(Selection& aSelection,
const nsAString& aBlockType,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aCancel && aHandled);
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
OwningNonNull<nsIAtom> blockType = NS_Atomize(aBlockType);
WillInsert(aSelection, aCancel);
// We want to ignore result of WillInsert()
*aCancel = false;
*aHandled = false;
nsresult rv = NormalizeSelection(&aSelection);
NS_ENSURE_SUCCESS(rv, rv);
AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
AutoTransactionsConserveSelection dontSpazMySelection(htmlEditor);
*aHandled = true;
// Contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetNodesFromSelection(aSelection, EditAction::makeBasicBlock,
arrayOfNodes);
NS_ENSURE_SUCCESS(rv, rv);
// Remove all non-editable nodes. Leave them be.
for (int32_t i = arrayOfNodes.Length() - 1; i >= 0; i--) {
if (!htmlEditor->IsEditable(arrayOfNodes[i])) {
arrayOfNodes.RemoveElementAt(i);
}
}
// If nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
// Get selection location
NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
aSelection.GetRangeAt(0)->GetStartParent());
OwningNonNull<nsINode> parent =
*aSelection.GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
if (blockType == nsGkAtoms::normal ||
blockType == nsGkAtoms::_empty) {
// We are removing blocks (going to "body text")
NS_ENSURE_TRUE(htmlEditor->GetBlock(parent), NS_ERROR_NULL_POINTER);
OwningNonNull<Element> curBlock = *htmlEditor->GetBlock(parent);
if (HTMLEditUtils::IsFormatNode(curBlock)) {
// 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> brNode =
htmlEditor->GetNextHTMLNode(parent, offset);
if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
rv = htmlEditor->DeleteNode(brNode);
NS_ENSURE_SUCCESS(rv, rv);
}
// Do the splits!
offset = htmlEditor->SplitNodeDeep(curBlock, *parent->AsContent(),
offset,
HTMLEditor::EmptyContainers::no);
NS_ENSURE_STATE(offset != -1);
// Put a br at the split point
brNode = htmlEditor->CreateBR(curBlock->GetParentNode(), offset);
NS_ENSURE_STATE(brNode);
// Put selection at the split point
*aHandled = true;
rv = aSelection.Collapse(curBlock->GetParentNode(), offset);
// Don't restore the selection
selectionRestorer.Abort();
NS_ENSURE_SUCCESS(rv, rv);
}
// Else nothing to do!
} else {
// We are making a block. Consume a br, if needed.
nsCOMPtr<nsIContent> brNode =
htmlEditor->GetNextHTMLNode(parent, offset, true);
if (brNode && brNode->IsHTMLElement(nsGkAtoms::br)) {
rv = htmlEditor->DeleteNode(brNode);
NS_ENSURE_SUCCESS(rv, rv);
// We don't need to act on this node any more
arrayOfNodes.RemoveElement(brNode);
}
// Make sure we can put a block here
rv = SplitAsNeeded(blockType, parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<Element> block =
htmlEditor->CreateNode(blockType, parent, offset);
NS_ENSURE_STATE(block);
// 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 = htmlEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
arrayOfNodes.RemoveElementAt(0);
}
// Put selection in new block
*aHandled = true;
rv = aSelection.Collapse(block, 0);
// Don't restore the selection
selectionRestorer.Abort();
NS_ENSURE_SUCCESS(rv, 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);
NS_ENSURE_SUCCESS(rv, rv);
} else if (blockType == nsGkAtoms::normal ||
blockType == nsGkAtoms::_empty) {
rv = RemoveBlockStyle(arrayOfNodes);
NS_ENSURE_SUCCESS(rv, rv);
} else {
rv = ApplyBlockStyle(arrayOfNodes, blockType);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult
HTMLEditRules::DidMakeBasicBlock(Selection* aSelection,
RulesInfo* aInfo,
nsresult aResult)
{
NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
// check for empty block. if so, put a moz br in it.
if (!aSelection->Collapsed()) {
return NS_OK;
}
NS_ENSURE_STATE(aSelection->GetRangeAt(0) &&
aSelection->GetRangeAt(0)->GetStartParent());
nsresult rv =
InsertMozBRIfNeeded(*aSelection->GetRangeAt(0)->GetStartParent());
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
HTMLEditRules::WillIndent(Selection* aSelection,
bool* aCancel,
bool* aHandled)
{
NS_ENSURE_STATE(mHTMLEditor);
if (mHTMLEditor->IsCSSEnabled()) {
nsresult rv = WillCSSIndent(aSelection, aCancel, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
nsresult rv = WillHTMLIndent(aSelection, aCancel, aHandled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::WillCSSIndent(Selection* aSelection,
bool* aCancel,
bool* aHandled)
{
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
WillInsert(*aSelection, aCancel);
// initialize out param
// we want to ignore result of WillInsert()
*aCancel = false;
*aHandled = true;
nsresult rv = NormalizeSelection(aSelection);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
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 (aSelection->Collapsed()) {
nsCOMPtr<nsINode> node;
int32_t offset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(aSelection,
getter_AddRefs(node), &offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*node);
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
rv = GetNodesFromSelection(*aSelection, EditAction::indent, arrayOfNodes);
NS_ENSURE_SUCCESS(rv, rv);
}
// if nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
// get selection location
NS_ENSURE_STATE(aSelection->RangeCount());
nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_STATE(parent);
// make sure we can put a block here
rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::div,
parent, offset);
NS_ENSURE_STATE(theBlock);
// remember our new block for postprocessing
mNewBlock = theBlock;
ChangeIndentation(*theBlock, Change::plus);
// delete anything that was in the list of nodes
while (!arrayOfNodes.IsEmpty()) {
OwningNonNull<nsINode> curNode = arrayOfNodes[0];
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
arrayOfNodes.RemoveElementAt(0);
}
// put selection in new block
*aHandled = true;
rv = aSelection->Collapse(theBlock, 0);
// Don't restore the selection
selectionRestorer.Abort();
return rv;
}
// Ok, now go through all the nodes and put them in a blockquote,
// or whatever is appropriate. Wohoo!
nsCOMPtr<nsINode> curParent;
nsCOMPtr<Element> curList, curQuote;
nsCOMPtr<nsIContent> sibling;
int32_t listCount = arrayOfNodes.Length();
for (int32_t i = 0; i < listCount; i++) {
// here's where we actually figure out what to do
NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent();
// Ignore all non-editable nodes. Leave them be.
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsEditable(curNode)) {
continue;
}
curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
// some logic for putting list items into nested lists...
if (HTMLEditUtils::IsList(curParent)) {
sibling = nullptr;
// 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.
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetNextHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
curParent->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
curParent->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, sibling, 0);
NS_ENSURE_SUCCESS(rv, 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.
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
curParent->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
curParent->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, sibling, -1);
NS_ENSURE_SUCCESS(rv, rv);
continue;
}
sibling = nullptr;
// check to see if curList is still appropriate. Which it is if
// curNode is still right after it in the same list.
if (curList) {
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
// create a new nested list of correct type
rv =
SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
curParent, offset);
NS_ENSURE_STATE(curList);
// 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
uint32_t listLen = curList->Length();
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curList, listLen);
NS_ENSURE_SUCCESS(rv, rv);
}
// Not a list item.
else {
if (curNode && IsBlockNode(*curNode)) {
ChangeIndentation(*curNode->AsElement(), Change::plus);
curQuote = nullptr;
} else {
if (!curQuote) {
// First, check that our element can contain a div.
if (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_UNEXPECTED;
}
if (!mHTMLEditor->CanContainTag(*curParent, *nsGkAtoms::div)) {
return NS_OK; // cancelled
}
rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curQuote = mHTMLEditor->CreateNode(nsGkAtoms::div, curParent,
offset);
NS_ENSURE_STATE(curQuote);
ChangeIndentation(*curQuote, Change::plus);
// 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
uint32_t quoteLen = curQuote->Length();
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curQuote, quoteLen);
NS_ENSURE_SUCCESS(rv, rv);
}
}
}
return NS_OK;
}
nsresult
HTMLEditRules::WillHTMLIndent(Selection* aSelection,
bool* aCancel,
bool* aHandled)
{
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
WillInsert(*aSelection, aCancel);
// initialize out param
// we want to ignore result of WillInsert()
*aCancel = false;
*aHandled = true;
nsresult rv = NormalizeSelection(aSelection);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
// 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(*aSelection, arrayOfRanges, EditAction::indent);
// use these ranges to contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes, EditAction::indent);
NS_ENSURE_SUCCESS(rv, rv);
// if nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
// get selection location
NS_ENSURE_STATE(aSelection->RangeCount());
nsCOMPtr<nsINode> parent = aSelection->GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection->GetRangeAt(0)->StartOffset();
NS_ENSURE_STATE(parent);
// make sure we can put a block here
rv = SplitAsNeeded(*nsGkAtoms::blockquote, parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> theBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote,
parent, offset);
NS_ENSURE_STATE(theBlock);
// 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];
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
arrayOfNodes.RemoveElementAt(0);
}
// put selection in new block
*aHandled = true;
rv = aSelection->Collapse(theBlock, 0);
// Don't restore the selection
selectionRestorer.Abort();
return rv;
}
// Ok, now go through all the nodes and put them in a blockquote,
// or whatever is appropriate. Wohoo!
nsCOMPtr<nsINode> curParent;
nsCOMPtr<nsIContent> sibling;
nsCOMPtr<Element> curList, curQuote, indentedLI;
int32_t listCount = arrayOfNodes.Length();
for (int32_t i = 0; i < listCount; i++) {
// here's where we actually figure out what to do
NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
nsCOMPtr<nsIContent> curNode = arrayOfNodes[i]->AsContent();
// Ignore all non-editable nodes. Leave them be.
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsEditable(curNode)) {
continue;
}
curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
// some logic for putting list items into nested lists...
if (HTMLEditUtils::IsList(curParent)) {
sibling = nullptr;
// 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.
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetNextHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
curParent->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
curParent->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, sibling, 0);
NS_ENSURE_SUCCESS(rv, 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.
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
if (sibling && HTMLEditUtils::IsList(sibling) &&
curParent->NodeInfo()->NameAtom() ==
sibling->NodeInfo()->NameAtom() &&
curParent->NodeInfo()->NamespaceID() ==
sibling->NodeInfo()->NamespaceID()) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, sibling, -1);
NS_ENSURE_SUCCESS(rv, rv);
continue;
}
sibling = nullptr;
// check to see if curList is still appropriate. Which it is if
// curNode is still right after it in the same list.
if (curList) {
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
// create a new nested list of correct type
rv =
SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
curParent, offset);
NS_ENSURE_STATE(curList);
// 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
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
// forget curQuote, if any
curQuote = nullptr;
}
// Not a list item, use blockquote?
else {
// 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.
nsCOMPtr<Element> listItem = IsInListItem(curNode);
if (listItem) {
if (indentedLI == listItem) {
// already indented this list item
continue;
}
curParent = listItem->GetParentNode();
offset = curParent ? curParent->IndexOf(listItem) : -1;
// 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 = nullptr;
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
// create a new nested list of correct type
rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent,
offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curList = mHTMLEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
curParent, offset);
NS_ENSURE_STATE(curList);
}
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(listItem, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
// remember we indented this li
indentedLI = listItem;
} else {
// 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 (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_UNEXPECTED;
}
if (!mHTMLEditor->CanContainTag(*curParent, *nsGkAtoms::blockquote)) {
return NS_OK; // cancelled
}
rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curQuote = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent,
offset);
NS_ENSURE_STATE(curQuote);
// 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
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->MoveNode(curNode, curQuote, -1);
NS_ENSURE_SUCCESS(rv, rv);
// forget curList, if any
curList = nullptr;
}
}
}
return NS_OK;
}
nsresult
HTMLEditRules::WillOutdent(Selection& aSelection,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aCancel && aHandled);
*aCancel = false;
*aHandled = true;
nsCOMPtr<nsIContent> rememberedLeftBQ, rememberedRightBQ;
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
bool useCSS = htmlEditor->IsCSSEnabled();
nsresult rv = NormalizeSelection(&aSelection);
NS_ENSURE_SUCCESS(rv, rv);
// Some scoping for selection resetting - we may need to tweak it
{
AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
// 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;
rv = GetNodesFromSelection(aSelection, EditAction::outdent, arrayOfNodes);
NS_ENSURE_SUCCESS(rv, rv);
// Okay, now go through all the nodes and remove a level of blockquoting,
// or whatever is appropriate. Wohoo!
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
nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
// 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) {
rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS,
getter_AddRefs(rememberedLeftBQ),
getter_AddRefs(rememberedRightBQ));
NS_ENSURE_SUCCESS(rv, rv);
curBlockQuote = nullptr;
firstBQChild = nullptr;
lastBQChild = nullptr;
curBlockQuoteIsIndentedWithCSS = false;
}
rv = htmlEditor->RemoveBlockContainer(curNode);
NS_ENSURE_SUCCESS(rv, rv);
continue;
}
// Is it a block with a 'margin' property?
if (useCSS && IsBlockNode(curNode)) {
nsIAtom& marginProperty =
MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode);
nsAutoString value;
htmlEditor->mCSSEditUtils->GetSpecifiedProperty(curNode,
marginProperty,
value);
float f;
nsCOMPtr<nsIAtom> unit;
NS_ENSURE_STATE(htmlEditor);
htmlEditor->mCSSEditUtils->ParseLength(value, &f,
getter_AddRefs(unit));
if (f > 0) {
ChangeIndentation(*curNode->AsElement(), Change::minus);
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) {
rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS,
getter_AddRefs(rememberedLeftBQ),
getter_AddRefs(rememberedRightBQ));
NS_ENSURE_SUCCESS(rv, rv);
curBlockQuote = nullptr;
firstBQChild = nullptr;
lastBQChild = nullptr;
curBlockQuoteIsIndentedWithCSS = false;
}
rv = PopListItem(*curNode->AsContent());
NS_ENSURE_SUCCESS(rv, 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].
rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS,
getter_AddRefs(rememberedLeftBQ),
getter_AddRefs(rememberedRightBQ));
NS_ENSURE_SUCCESS(rv, rv);
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) &&
htmlEditor->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;
} else if (useCSS) {
nsIAtom& marginProperty =
MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, curNode);
nsAutoString value;
htmlEditor->mCSSEditUtils->GetSpecifiedProperty(*n, marginProperty,
value);
float f;
nsCOMPtr<nsIAtom> unit;
htmlEditor->mCSSEditUtils->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) {
// Couldn't find enclosing blockquote. Handle list cases.
if (HTMLEditUtils::IsList(curParent)) {
// Move node out of list
if (HTMLEditUtils::IsList(curNode)) {
// Just unwrap this sublist
rv = htmlEditor->RemoveBlockContainer(curNode);
NS_ENSURE_SUCCESS(rv, rv);
}
// handled list item case above
} else 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);
NS_ENSURE_SUCCESS(rv, 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.
rv = htmlEditor->MoveNode(child, curParent, offset + 1);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Delete any non-list items for now
rv = htmlEditor->DeleteNode(child);
NS_ENSURE_SUCCESS(rv, rv);
}
child = curNode->GetLastChild();
}
// Delete the now-empty list
rv = htmlEditor->RemoveBlockContainer(curNode);
NS_ENSURE_SUCCESS(rv, rv);
} else 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) {
ChangeIndentation(*element, Change::minus);
}
}
}
}
if (curBlockQuote) {
// We have a blockquote we haven't finished handling
rv = OutdentPartOfBlock(*curBlockQuote, *firstBQChild, *lastBQChild,
curBlockQuoteIsIndentedWithCSS,
getter_AddRefs(rememberedLeftBQ),
getter_AddRefs(rememberedRightBQ));
NS_ENSURE_SUCCESS(rv, rv);
}
}
// Make sure selection didn't stick to last piece of content in old bq (only
// a problem for collapsed selections)
if (rememberedLeftBQ || rememberedRightBQ) {
if (aSelection.Collapsed()) {
// Push selection past end of rememberedLeftBQ
NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_OK);
nsCOMPtr<nsINode> startNode = aSelection.GetRangeAt(0)->GetStartParent();
int32_t startOffset = aSelection.GetRangeAt(0)->StartOffset();
if (rememberedLeftBQ &&
(startNode == rememberedLeftBQ ||
EditorUtils::IsDescendantOf(startNode, rememberedLeftBQ))) {
// Selection is inside rememberedLeftBQ - push it past it.
startNode = rememberedLeftBQ->GetParentNode();
startOffset = startNode ? 1 + startNode->IndexOf(rememberedLeftBQ) : 0;
aSelection.Collapse(startNode, startOffset);
}
// And pull selection before beginning of rememberedRightBQ
startNode = aSelection.GetRangeAt(0)->GetStartParent();
startOffset = aSelection.GetRangeAt(0)->StartOffset();
if (rememberedRightBQ &&
(startNode == rememberedRightBQ ||
EditorUtils::IsDescendantOf(startNode, rememberedRightBQ))) {
// Selection is inside rememberedRightBQ - push it before it.
startNode = rememberedRightBQ->GetParentNode();
startOffset = startNode ? startNode->IndexOf(rememberedRightBQ) : -1;
aSelection.Collapse(startNode, startOffset);
}
}
return NS_OK;
}
return NS_OK;
}
/**
* RemovePartOfBlock() splits aBlock and move aStartChild to aEndChild out of
* aBlock.
*/
nsresult
HTMLEditRules::RemovePartOfBlock(Element& aBlock,
nsIContent& aStartChild,
nsIContent& aEndChild)
{
SplitBlock(aBlock, aStartChild, aEndChild);
// Get rid of part of blockquote we are outdenting
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->RemoveBlockContainer(aBlock);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
void
HTMLEditRules::SplitBlock(Element& aBlock,
nsIContent& aStartChild,
nsIContent& aEndChild,
nsIContent** aOutLeftNode,
nsIContent** aOutRightNode,
nsIContent** aOutMiddleNode)
{
// aStartChild and aEndChild must be exclusive descendants of aBlock
MOZ_ASSERT(EditorUtils::IsDescendantOf(&aStartChild, &aBlock) &&
EditorUtils::IsDescendantOf(&aEndChild, &aBlock));
NS_ENSURE_TRUE_VOID(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// Get split point location
OwningNonNull<nsIContent> startParent = *aStartChild.GetParent();
int32_t startOffset = startParent->IndexOf(&aStartChild);
// Do the splits!
nsCOMPtr<nsIContent> newMiddleNode1;
htmlEditor->SplitNodeDeep(aBlock, startParent, startOffset,
HTMLEditor::EmptyContainers::no,
aOutLeftNode, getter_AddRefs(newMiddleNode1));
// Get split point location
OwningNonNull<nsIContent> endParent = *aEndChild.GetParent();
// +1 because we want to be after the child
int32_t endOffset = 1 + endParent->IndexOf(&aEndChild);
// Do the splits!
nsCOMPtr<nsIContent> newMiddleNode2;
htmlEditor->SplitNodeDeep(aBlock, endParent, endOffset,
HTMLEditor::EmptyContainers::no,
getter_AddRefs(newMiddleNode2), aOutRightNode);
if (aOutMiddleNode) {
if (newMiddleNode2) {
newMiddleNode2.forget(aOutMiddleNode);
} else {
newMiddleNode1.forget(aOutMiddleNode);
}
}
}
nsresult
HTMLEditRules::OutdentPartOfBlock(Element& aBlock,
nsIContent& aStartChild,
nsIContent& aEndChild,
bool aIsBlockIndentedWithCSS,
nsIContent** aOutLeftNode,
nsIContent** aOutRightNode)
{
MOZ_ASSERT(aOutLeftNode && aOutRightNode);
nsCOMPtr<nsIContent> middleNode;
SplitBlock(aBlock, aStartChild, aEndChild, aOutLeftNode, aOutRightNode,
getter_AddRefs(middleNode));
NS_ENSURE_STATE(middleNode);
if (!aIsBlockIndentedWithCSS) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->RemoveBlockContainer(*middleNode);
NS_ENSURE_SUCCESS(rv, rv);
} else if (middleNode->IsElement()) {
// We do nothing if middleNode isn't an element
nsresult rv = ChangeIndentation(*middleNode->AsElement(), Change::minus);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
/**
* ConvertListType() converts list type and list item type.
*/
nsresult
HTMLEditRules::ConvertListType(Element* aList,
Element** aOutList,
nsIAtom* aListType,
nsIAtom* aItemType)
{
MOZ_ASSERT(aList);
MOZ_ASSERT(aOutList);
MOZ_ASSERT(aListType);
MOZ_ASSERT(aItemType);
nsCOMPtr<nsINode> child = aList->GetFirstChild();
while (child) {
if (child->IsElement()) {
dom::Element* element = child->AsElement();
if (HTMLEditUtils::IsListItem(element) &&
!element->IsHTMLElement(aItemType)) {
child = mHTMLEditor->ReplaceContainer(element, aItemType);
NS_ENSURE_STATE(child);
} else if (HTMLEditUtils::IsList(element) &&
!element->IsHTMLElement(aListType)) {
nsCOMPtr<dom::Element> temp;
nsresult rv = ConvertListType(child->AsElement(), getter_AddRefs(temp),
aListType, aItemType);
NS_ENSURE_SUCCESS(rv, rv);
child = temp.forget();
}
}
child = child->GetNextSibling();
}
if (aList->IsHTMLElement(aListType)) {
nsCOMPtr<dom::Element> list = aList->AsElement();
list.forget(aOutList);
return NS_OK;
}
*aOutList = mHTMLEditor->ReplaceContainer(aList, aListType).take();
NS_ENSURE_STATE(aOutList);
return NS_OK;
}
/**
* CreateStyleForInsertText() takes care of clearing and setting appropriate
* style nodes for text insertion.
*/
nsresult
HTMLEditRules::CreateStyleForInsertText(Selection& aSelection,
nsIDocument& aDoc)
{
MOZ_ASSERT(mHTMLEditor->mTypeInState);
bool weDidSomething = false;
NS_ENSURE_STATE(aSelection.GetRangeAt(0));
nsCOMPtr<nsINode> node = aSelection.GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
// next examine our present style and make sure default styles are either
// present or explicitly overridden. If neither, add the default style to
// the TypeInState
int32_t length = mHTMLEditor->mDefaultStyles.Length();
for (int32_t j = 0; j < length; j++) {
PropItem* propItem = mHTMLEditor->mDefaultStyles[j];
MOZ_ASSERT(propItem);
bool bFirst, bAny, bAll;
// GetInlineProperty also examine TypeInState. The only gotcha here is
// that a cleared property looks like an unset property. For now I'm
// assuming that's not a problem: that default styles will always be
// multivalue styles (like font face or size) where clearing the style
// means we want to go back to the default. If we ever wanted a "toggle"
// style like bold for a default, though, I'll have to add code to detect
// the difference between unset and explicitly cleared, else user would
// never be able to unbold, for instance.
nsAutoString curValue;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetInlinePropertyBase(*propItem->tag, &propItem->attr,
nullptr, &bFirst, &bAny, &bAll,
&curValue, false);
NS_ENSURE_SUCCESS(rv, rv);
if (!bAny) {
// no style set for this prop/attr
mHTMLEditor->mTypeInState->SetProp(propItem->tag, propItem->attr,
propItem->value);
}
}
nsCOMPtr<Element> rootElement = aDoc.GetRootElement();
NS_ENSURE_STATE(rootElement);
// process clearing any styles first
UniquePtr<PropItem> item =
Move(mHTMLEditor->mTypeInState->TakeClearProperty());
while (item && node != rootElement) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->ClearStyle(address_of(node), &offset,
item->tag, &item->attr);
NS_ENSURE_SUCCESS(rv, rv);
item = Move(mHTMLEditor->mTypeInState->TakeClearProperty());
weDidSomething = true;
}
// then process setting any styles
int32_t relFontSize = mHTMLEditor->mTypeInState->TakeRelativeFontSize();
item = Move(mHTMLEditor->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
NS_ENSURE_STATE(mHTMLEditor);
offset = mHTMLEditor->SplitNodeDeep(*text, *text, offset);
NS_ENSURE_STATE(offset != -1);
node = node->GetParentNode();
}
if (!mHTMLEditor->IsContainer(node)) {
return NS_OK;
}
OwningNonNull<Text> newNode = aDoc.CreateTextNode(EmptyString());
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->InsertNode(*newNode, *node, offset);
NS_ENSURE_SUCCESS(rv, 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++) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->RelativeFontChangeOnTextNode(dir, newNode, 0, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
}
while (item) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->SetInlinePropertyOnNode(*node->AsContent(),
*item->tag, &item->attr,
item->value);
NS_ENSURE_SUCCESS(rv, rv);
item = mHTMLEditor->mTypeInState->TakeSetProperty();
}
}
if (weDidSomething) {
return aSelection.Collapse(node, offset);
}
return NS_OK;
}
/**
* Figure out if aNode is (or is inside) an empty block. A block can have
* children and still be considered empty, if the children are empty or
* non-editable.
*/
nsresult
HTMLEditRules::IsEmptyBlock(Element& aNode,
bool* aOutIsEmptyBlock,
MozBRCounts aMozBRCounts)
{
MOZ_ASSERT(aOutIsEmptyBlock);
*aOutIsEmptyBlock = true;
NS_ENSURE_TRUE(IsBlockNode(aNode), NS_ERROR_NULL_POINTER);
return mHTMLEditor->IsEmptyNode(aNode.AsDOMNode(), aOutIsEmptyBlock,
aMozBRCounts == MozBRCounts::yes ? false
: true);
}
nsresult
HTMLEditRules::WillAlign(Selection& aSelection,
const nsAString& aAlignType,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aCancel && aHandled);
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
WillInsert(aSelection, aCancel);
// Initialize out param. We want to ignore result of WillInsert().
*aCancel = false;
*aHandled = false;
nsresult rv = NormalizeSelection(&aSelection);
NS_ENSURE_SUCCESS(rv, rv);
AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
// 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;
nsTArray<OwningNonNull<nsINode>> nodeArray;
rv = GetNodesFromSelection(aSelection, EditAction::align, nodeArray);
NS_ENSURE_SUCCESS(rv, 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(GetAsDOMNode(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, ContentsOnly::yes);
NS_ENSURE_SUCCESS(rv, 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
NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
aSelection.GetRangeAt(0)->GetStartParent());
OwningNonNull<nsINode> parent =
*aSelection.GetRangeAt(0)->GetStartParent();
emptyDiv = !HTMLEditUtils::IsTableElement(parent) ||
HTMLEditUtils::IsTableCellOrCaption(parent);
}
}
if (emptyDiv) {
nsCOMPtr<nsINode> parent =
aSelection.GetRangeAt(0) ? aSelection.GetRangeAt(0)->GetStartParent()
: nullptr;
NS_ENSURE_STATE(parent);
int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
// Consume a trailing br, if any. This is to keep an alignment from
// creating extra lines, if possible.
nsCOMPtr<nsIContent> brContent =
htmlEditor->GetNextHTMLNode(parent, offset);
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 = htmlEditor->GetNextHTMLSibling(parent,
offset);
if (sibling && !IsBlockNode(*sibling)) {
rv = htmlEditor->DeleteNode(brContent);
NS_ENSURE_SUCCESS(rv, rv);
}
}
nsCOMPtr<Element> div = htmlEditor->CreateNode(nsGkAtoms::div, parent,
offset);
NS_ENSURE_STATE(div);
// Remember our new block for postprocessing
mNewBlock = div;
// Set up the alignment on the div, using HTML or CSS
rv = AlignBlock(*div, aAlignType, ContentsOnly::yes);
NS_ENSURE_SUCCESS(rv, rv);
*aHandled = true;
// Put in a moz-br so that it won't get deleted
rv = CreateMozBR(div->AsDOMNode(), 0);
NS_ENSURE_SUCCESS(rv, rv);
rv = aSelection.Collapse(div, 0);
// Don't restore the selection
selectionRestorer.Abort();
NS_ENSURE_SUCCESS(rv, rv);
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 = htmlEditor->IsCSSEnabled();
for (size_t i = 0; i < nodeArray.Length(); i++) {
auto& curNode = nodeArray[i];
// Here's where we actually figure out what to do
// Ignore all non-editable nodes. Leave them be.
if (!htmlEditor->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(GetAsDOMNode(curNode))) {
rv = AlignBlock(*curNode->AsElement(), aAlignType, ContentsOnly::no);
NS_ENSURE_SUCCESS(rv, rv);
// Clear out curDiv so that we don't put nodes after this one into it
curDiv = nullptr;
continue;
}
nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
// Skip insignificant formatting text nodes to prevent unnecessary
// structure splitting!
bool isEmptyTextNode = false;
if (curNode->GetAsText() &&
((HTMLEditUtils::IsTableElement(curParent) &&
!HTMLEditUtils::IsTableCellOrCaption(*curParent)) ||
HTMLEditUtils::IsList(curParent) ||
(NS_SUCCEEDED(htmlEditor->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)) {
rv = RemoveAlignment(GetAsDOMNode(curNode), aAlignType, true);
NS_ENSURE_SUCCESS(rv, rv);
if (useCSS) {
htmlEditor->mCSSEditUtils->SetCSSEquivalentToHTMLStyle(
curNode->AsElement(), nullptr, nsGkAtoms::align,
&aAlignType, false);
curDiv = nullptr;
continue;
}
if (HTMLEditUtils::IsList(curParent)) {
// If we don't use CSS, add a contraint to list element: they have to
// be inside another list, i.e., >= second level of nesting
rv = AlignInnerBlocks(*curNode, &aAlignType);
NS_ENSURE_SUCCESS(rv, 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[i]) {
// First, check that our element can contain a div.
if (!htmlEditor->CanContainTag(*curParent, *nsGkAtoms::div)) {
// Cancelled
return NS_OK;
}
rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
curDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent, offset);
NS_ENSURE_STATE(curDiv);
// Remember our new block for postprocessing
mNewBlock = curDiv;
// Set up the alignment on the div
rv = AlignBlock(*curDiv, aAlignType, ContentsOnly::yes);
}
NS_ENSURE_STATE(curNode->IsContent());
// Tuck the node into the end of the active div
rv = htmlEditor->MoveNode(curNode->AsContent(), curDiv, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
/**
* AlignInnerBlocks() aligns inside table cells or list items.
*/
nsresult
HTMLEditRules::AlignInnerBlocks(nsINode& aNode,
const nsAString* alignType)
{
NS_ENSURE_TRUE(alignType, NS_ERROR_NULL_POINTER);
// 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(GetAsDOMNode(node), alignType);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
/**
* AlignBlockContents() aligns contents of a block element.
*/
nsresult
HTMLEditRules::AlignBlockContents(nsIDOMNode* aNode,
const nsAString* alignType)
{
nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
NS_ENSURE_TRUE(node && alignType, NS_ERROR_NULL_POINTER);
nsCOMPtr<nsIContent> firstChild, lastChild;
bool useCSS = mHTMLEditor->IsCSSEnabled();
NS_ENSURE_STATE(mHTMLEditor);
firstChild = mHTMLEditor->GetFirstEditableChild(*node);
NS_ENSURE_STATE(mHTMLEditor);
lastChild = mHTMLEditor->GetLastEditableChild(*node);
if (!firstChild) {
// this cell has no content, nothing to align
} else if (firstChild == lastChild &&
firstChild->IsHTMLElement(nsGkAtoms::div)) {
// the cell already has a div containing all of its content: just
// act on this div.
RefPtr<Element> divElem = firstChild->AsElement();
if (useCSS) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->SetAttributeOrEquivalent(divElem,
nsGkAtoms::align,
*alignType, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->SetAttribute(divElem, nsGkAtoms::align,
*alignType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
} else {
// else we need to put in a div, set the alignment, and toss in all the children
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Element> divElem = mHTMLEditor->CreateNode(nsGkAtoms::div, node, 0);
NS_ENSURE_STATE(divElem);
// set up the alignment on the div
if (useCSS) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->SetAttributeOrEquivalent(divElem, nsGkAtoms::align,
*alignType, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->SetAttribute(divElem, nsGkAtoms::align, *alignType);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// tuck the children into the end of the active div
while (lastChild && (lastChild != divElem)) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->MoveNode(lastChild, divElem, 0);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
lastChild = mHTMLEditor->GetLastEditableChild(*node);
}
}
return NS_OK;
}
/**
* CheckForEmptyBlock() is called by WillDeleteSelection() to detect and handle
* case of deleting from inside an empty block.
*/
nsresult
HTMLEditRules::CheckForEmptyBlock(nsINode* aStartNode,
Element* aBodyNode,
Selection* aSelection,
nsIEditor::EDirection aAction,
bool* aHandled)
{
// If the editing host is an inline element, bail out early.
if (aBodyNode && IsInlineNode(*aBodyNode)) {
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// If we are inside an empty block, delete it. Note: do NOT delete table
// elements this way.
nsCOMPtr<Element> block = htmlEditor->GetBlock(*aStartNode);
bool bIsEmptyNode;
nsCOMPtr<Element> emptyBlock;
if (block && block != aBodyNode) {
// Efficiency hack, avoiding IsEmptyNode() call when in body
nsresult rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
NS_ENSURE_SUCCESS(rv, rv);
while (block && bIsEmptyNode && !HTMLEditUtils::IsTableElement(block) &&
block != aBodyNode) {
emptyBlock = block;
block = htmlEditor->GetBlockNodeParent(emptyBlock);
if (block) {
rv = htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
NS_ENSURE_SUCCESS(rv, rv);
}
}
}
if (emptyBlock && emptyBlock->IsEditable()) {
nsCOMPtr<nsINode> blockParent = emptyBlock->GetParentNode();
NS_ENSURE_TRUE(blockParent, NS_ERROR_FAILURE);
int32_t offset = blockParent->IndexOf(emptyBlock);
if (HTMLEditUtils::IsListItem(emptyBlock)) {
// Are we the first list item in the list?
NS_ENSURE_STATE(htmlEditor);
if (htmlEditor->IsFirstEditableChild(emptyBlock)) {
nsCOMPtr<nsINode> listParent = blockParent->GetParentNode();
NS_ENSURE_TRUE(listParent, NS_ERROR_FAILURE);
int32_t listOffset = listParent->IndexOf(blockParent);
// If we are a sublist, skip the br creation
if (!HTMLEditUtils::IsList(listParent)) {
// Create a br before list
NS_ENSURE_STATE(htmlEditor);
nsCOMPtr<Element> br =
htmlEditor->CreateBR(listParent, listOffset);
NS_ENSURE_STATE(br);
// Adjust selection to be right before it
nsresult rv = aSelection->Collapse(listParent, listOffset);
NS_ENSURE_SUCCESS(rv, rv);
}
// Else just let selection percolate up. We'll adjust it in
// AfterEdit()
}
} else {
if (aAction == nsIEditor::eNext || aAction == nsIEditor::eNextWord ||
aAction == nsIEditor::eToEndOfLine) {
// Move to the start of the next node, if any
nsCOMPtr<nsIContent> nextNode = htmlEditor->GetNextNode(blockParent,
offset + 1, true);
if (nextNode) {
EditorDOMPoint pt = GetGoodSelPointForNode(*nextNode, aAction);
nsresult rv = aSelection->Collapse(pt.node, pt.offset);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Adjust selection to be right after it.
nsresult rv = aSelection->Collapse(blockParent, offset + 1);
NS_ENSURE_SUCCESS(rv, rv);
}
} else if (aAction == nsIEditor::ePrevious ||
aAction == nsIEditor::ePreviousWord ||
aAction == nsIEditor::eToBeginningOfLine) {
// Move to the end of the previous node
nsCOMPtr<nsIContent> priorNode = htmlEditor->GetPriorNode(blockParent,
offset,
true);
if (priorNode) {
EditorDOMPoint pt = GetGoodSelPointForNode(*priorNode, aAction);
nsresult rv = aSelection->Collapse(pt.node, pt.offset);
NS_ENSURE_SUCCESS(rv, rv);
} else {
nsresult rv = aSelection->Collapse(blockParent, offset + 1);
NS_ENSURE_SUCCESS(rv, rv);
}
} else if (aAction != nsIEditor::eNone) {
NS_RUNTIMEABORT("CheckForEmptyBlock doesn't support this action yet");
}
}
NS_ENSURE_STATE(htmlEditor);
*aHandled = true;
nsresult rv = htmlEditor->DeleteNode(emptyBlock);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
Element*
HTMLEditRules::CheckForInvisibleBR(Element& aBlock,
BRLocation aWhere,
int32_t aOffset)
{
nsCOMPtr<nsINode> testNode;
int32_t testOffset = 0;
if (aWhere == BRLocation::blockEnd) {
// No block crossing
nsCOMPtr<nsIContent> rightmostNode =
mHTMLEditor->GetRightmostChild(&aBlock, true);
if (!rightmostNode) {
return nullptr;
}
testNode = rightmostNode->GetParentNode();
// Use offset + 1, so last node is included in our evaluation
testOffset = testNode->IndexOf(rightmostNode) + 1;
} else if (aOffset) {
testNode = &aBlock;
// We'll check everything to the left of the input position
testOffset = aOffset;
} else {
return nullptr;
}
WSRunObject wsTester(mHTMLEditor, testNode, testOffset);
if (WSType::br == wsTester.mStartReason) {
return wsTester.mStartReasonNode->AsElement();
}
return nullptr;
}
/**
* aLists and aTables allow the caller to specify what kind of content to
* "look inside". If aTables is Tables::yes, look inside any table content,
* and insert the inner content into the supplied issupportsarray at offset
* aIndex. Similarly with aLists and list content. aIndex is updated to
* point past inserted elements.
*/
void
HTMLEditRules::GetInnerContent(
nsINode& aNode,
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
int32_t* aIndex,
Lists aLists,
Tables aTables)
{
MOZ_ASSERT(aIndex);
for (nsCOMPtr<nsIContent> node = mHTMLEditor->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)++;
}
}
}
/**
* Promotes selection to include blocks that have all their children selected.
*/
nsresult
HTMLEditRules::ExpandSelectionForDeletion(Selection& aSelection)
{
// Don't need to touch collapsed selections
if (aSelection.Collapsed()) {
return NS_OK;
}
// We don't need to mess with cell selections, and we assume multirange
// selections are those.
if (aSelection.RangeCount() != 1) {
return NS_OK;
}
// Find current sel start and end
NS_ENSURE_TRUE(aSelection.GetRangeAt(0), NS_ERROR_NULL_POINTER);
OwningNonNull<nsRange> range = *aSelection.GetRangeAt(0);
nsCOMPtr<nsINode> selStartNode = range->GetStartParent();
int32_t selStartOffset = range->StartOffset();
nsCOMPtr<nsINode> selEndNode = range->GetEndParent();
int32_t selEndOffset = range->EndOffset();
// Find current selection common block parent
nsCOMPtr<Element> selCommon =
HTMLEditor::GetBlock(*range->GetCommonAncestor());
NS_ENSURE_STATE(selCommon);
// Set up for loops and cache our root element
nsCOMPtr<nsINode> firstBRParent;
nsCOMPtr<nsINode> unused;
int32_t visOffset = 0, firstBROffset = 0;
WSType wsType;
nsCOMPtr<Element> root = mHTMLEditor->GetActiveEditingHost();
NS_ENSURE_TRUE(root, NS_ERROR_FAILURE);
// Find previous visible thingy before start of selection
if (selStartNode != selCommon && selStartNode != root) {
while (true) {
WSRunObject wsObj(mHTMLEditor, selStartNode, selStartOffset);
wsObj.PriorVisibleNode(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->IndexOf(wsObj.mStartReasonNode) : -1;
}
}
// Find next visible thingy after end of selection
if (selEndNode != selCommon && selEndNode != root) {
for (;;) {
WSRunObject wsObj(mHTMLEditor, selEndNode, selEndOffset);
wsObj.NextVisibleNode(selEndNode, selEndOffset, address_of(unused),
&visOffset, &wsType);
if (wsType == WSType::br) {
if (mHTMLEditor->IsVisBreak(wsObj.mEndReasonNode)) {
break;
}
if (!firstBRParent) {
firstBRParent = selEndNode;
firstBROffset = selEndOffset;
}
selEndNode = wsObj.mEndReasonNode->GetParentNode();
selEndOffset = selEndNode
? selEndNode->IndexOf(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->IndexOf(wsObj.mEndReasonNode);
} else {
break;
}
}
}
// Now set the selection to the new range
aSelection.Collapse(selStartNode, selStartOffset);
// 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->SetStart(selStartNode, selStartOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = range->SetEnd(selEndNode, selEndOffset);
NS_ENSURE_SUCCESS(rv, 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 = aSelection.Extend(selEndNode, selEndOffset);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Only expand to just before br
nsresult rv = aSelection.Extend(firstBRParent, firstBROffset);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
/**
* 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.
*/
nsresult
HTMLEditRules::NormalizeSelection(Selection* inSelection)
{
NS_ENSURE_TRUE(inSelection, NS_ERROR_NULL_POINTER);
// don't need to touch collapsed selections
if (inSelection->Collapsed()) {
return NS_OK;
}
int32_t rangeCount;
nsresult rv = inSelection->GetRangeCount(&rangeCount);
NS_ENSURE_SUCCESS(rv, rv);
// we don't need to mess with cell selections, and we assume multirange selections are those.
if (rangeCount != 1) {
return NS_OK;
}
RefPtr<nsRange> range = inSelection->GetRangeAt(0);
NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
nsCOMPtr<nsIDOMNode> startNode, endNode;
int32_t startOffset, endOffset;
nsCOMPtr<nsIDOMNode> newStartNode, newEndNode;
int32_t newStartOffset, newEndOffset;
rv = range->GetStartContainer(getter_AddRefs(startNode));
NS_ENSURE_SUCCESS(rv, rv);
rv = range->GetStartOffset(&startOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = range->GetEndContainer(getter_AddRefs(endNode));
NS_ENSURE_SUCCESS(rv, rv);
rv = range->GetEndOffset(&endOffset);
NS_ENSURE_SUCCESS(rv, rv);
// adjusted values default to original values
newStartNode = startNode;
newStartOffset = startOffset;
newEndNode = endNode;
newEndOffset = endOffset;
// some locals we need for whitespace code
nsCOMPtr<nsINode> unused;
int32_t offset;
WSType wsType;
// let the whitespace code do the heavy lifting
WSRunObject wsEndObj(mHTMLEditor, endNode, endOffset);
// is there any intervening visible whitespace? if so we can't push selection past that,
// it would visibly change maening of users selection
nsCOMPtr<nsINode> endNode_(do_QueryInterface(endNode));
wsEndObj.PriorVisibleNode(endNode_, endOffset, address_of(unused),
&offset, &wsType);
if (wsType != WSType::text && wsType != WSType::normalWS) {
// eThisBlock and eOtherBlock conveniently distinquish 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.
nsCOMPtr<nsIDOMNode> child =
GetAsDOMNode(mHTMLEditor->GetRightmostChild(wsEndObj.mStartReasonNode,
true));
if (child) {
newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset);
++newEndOffset; // offset *after* child
}
// 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
nsCOMPtr<nsIDOMNode> child;
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->GetPriorHTMLNode(endNode, endOffset, address_of(child));
if (child) {
newEndNode = EditorBase::GetNodeLocation(child, &newEndOffset);
++newEndOffset; // offset *after* child
}
// 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.
newEndNode =
EditorBase::GetNodeLocation(GetAsDOMNode(wsEndObj.mStartReasonNode),
&newEndOffset);
}
}
// similar dealio for start of range
WSRunObject wsStartObj(mHTMLEditor, startNode, startOffset);
// is there any intervening visible whitespace? if so we can't push selection past that,
// it would visibly change maening of users selection
nsCOMPtr<nsINode> startNode_(do_QueryInterface(startNode));
wsStartObj.NextVisibleNode(startNode_, startOffset, address_of(unused),
&offset, &wsType);
if (wsType != WSType::text && wsType != WSType::normalWS) {
// eThisBlock and eOtherBlock conveniently distinquish 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.
nsCOMPtr<nsIDOMNode> child =
GetAsDOMNode(mHTMLEditor->GetLeftmostChild(wsStartObj.mEndReasonNode,
true));
if (child) {
newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset);
}
// 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
nsCOMPtr<nsIDOMNode> child;
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->GetNextHTMLNode(startNode, startOffset, address_of(child));
if (child) {
newStartNode = EditorBase::GetNodeLocation(child, &newStartOffset);
}
// 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.
newStartNode =
EditorBase::GetNodeLocation(GetAsDOMNode(wsStartObj.mEndReasonNode),
&newStartOffset);
++newStartOffset; // offset *after* break
}
}
// 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: ie, 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.
inSelection->Collapse(newStartNode, newStartOffset);
inSelection->Extend(newEndNode, newEndOffset);
return NS_OK;
}
/**
* GetPromotedPoint() figures out where a start or end point for a block
* operation really is.
*/
void
HTMLEditRules::GetPromotedPoint(RulesEndpoint aWhere,
nsIDOMNode* aNode,
int32_t aOffset,
EditAction actionID,
nsCOMPtr<nsIDOMNode>* outNode,
int32_t* outOffset)
{
nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
MOZ_ASSERT(node && outNode && outOffset);
// default values
*outNode = node->AsDOMNode();
*outOffset = aOffset;
// we do one thing for text actions, something else entirely for other
// actions
if (actionID == EditAction::insertText ||
actionID == EditAction::insertIMEText ||
actionID == EditAction::insertBreak ||
actionID == EditAction::deleteText) {
bool isSpace, isNBSP;
nsCOMPtr<nsIContent> content = do_QueryInterface(node), temp;
// 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) {
NS_ENSURE_TRUE_VOID(mHTMLEditor);
mHTMLEditor->IsPrevCharInNodeWhitespace(content, *outOffset,
&isSpace, &isNBSP,
getter_AddRefs(temp), &offset);
} else {
NS_ENSURE_TRUE_VOID(mHTMLEditor);
mHTMLEditor->IsNextCharInNodeWhitespace(content, *outOffset,
&isSpace, &isNBSP,
getter_AddRefs(temp), &offset);
}
if (isSpace || isNBSP) {
content = temp;
*outOffset = offset;
} else {
break;
}
}
*outNode = content->AsDOMNode();
return;
}
int32_t offset = 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 (node->IsNodeOfType(nsINode::eTEXT)) {
if (!node->GetParentNode()) {
// Okay, can't promote any further
return;
}
offset = node->GetParentNode()->IndexOf(node);
node = node->GetParentNode();
}
// look back through any further inline nodes that aren't across a <br>
// from us, and that are enclosed in the same block.
NS_ENSURE_TRUE_VOID(mHTMLEditor);
nsCOMPtr<nsINode> priorNode =
mHTMLEditor->GetPriorHTMLNode(node, offset, true);
while (priorNode && priorNode->GetParentNode() &&
mHTMLEditor && !mHTMLEditor->IsVisBreak(priorNode) &&
!IsBlockNode(*priorNode)) {
offset = priorNode->GetParentNode()->IndexOf(priorNode);
node = priorNode->GetParentNode();
NS_ENSURE_TRUE_VOID(mHTMLEditor);
priorNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true);
}
// 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.
NS_ENSURE_TRUE_VOID(mHTMLEditor);
nsCOMPtr<nsIContent> nearNode =
mHTMLEditor->GetPriorHTMLNode(node, offset, true);
while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) &&
node->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 (actionID == EditAction::outdent &&
node->IsHTMLElement(nsGkAtoms::blockquote)) {
break;
}
int32_t parentOffset = node->GetParentNode()->IndexOf(node);
nsCOMPtr<nsINode> parent = node->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 we're not performing a block-level action.
bool blockLevelAction = actionID == EditAction::indent ||
actionID == EditAction::outdent ||
actionID == EditAction::align ||
actionID == EditAction::makeBasicBlock;
NS_ENSURE_TRUE_VOID(mHTMLEditor);
if (!mHTMLEditor->IsDescendantOfEditorRoot(parent) &&
(blockLevelAction || !mHTMLEditor ||
!mHTMLEditor->IsDescendantOfEditorRoot(node))) {
NS_ENSURE_TRUE_VOID(mHTMLEditor);
break;
}
node = parent;
offset = parentOffset;
NS_ENSURE_TRUE_VOID(mHTMLEditor);
nearNode = mHTMLEditor->GetPriorHTMLNode(node, offset, true);
}
*outNode = node->AsDOMNode();
*outOffset = offset;
return;
}
// aWhere == kEnd
// some special casing for text nodes
if (node->IsNodeOfType(nsINode::eTEXT)) {
if (!node->GetParentNode()) {
// Okay, can't promote any further
return;
}
// want to be after the text node
offset = 1 + node->GetParentNode()->IndexOf(node);
node = node->GetParentNode();
}
// look ahead through any further inline nodes that aren't across a <br> from
// us, and that are enclosed in the same block.
NS_ENSURE_TRUE(mHTMLEditor, /* void */);
nsCOMPtr<nsIContent> nextNode =
mHTMLEditor->GetNextHTMLNode(node, offset, true);
while (nextNode && !IsBlockNode(*nextNode) && nextNode->GetParentNode()) {
offset = 1 + nextNode->GetParentNode()->IndexOf(nextNode);
node = nextNode->GetParentNode();
NS_ENSURE_TRUE_VOID(mHTMLEditor);
if (mHTMLEditor->IsVisBreak(nextNode)) {
break;
}
// Check for newlines in pre-formatted text nodes.
bool isPRE;
mHTMLEditor->IsPreformatted(nextNode->AsDOMNode(), &isPRE);
if (isPRE) {
nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(nextNode);
if (textNode) {
nsAutoString tempString;
textNode->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;
}
*outNode = nextNode->AsDOMNode();
*outOffset = newlinePos + 1;
return;
}
}
}
NS_ENSURE_TRUE_VOID(mHTMLEditor);
nextNode = mHTMLEditor->GetNextHTMLNode(node, offset, true);
}
// 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.
NS_ENSURE_TRUE_VOID(mHTMLEditor);
nsCOMPtr<nsIContent> nearNode =
mHTMLEditor->GetNextHTMLNode(node, offset, true);
while (!nearNode && !node->IsHTMLElement(nsGkAtoms::body) &&
node->GetParentNode()) {
int32_t parentOffset = node->GetParentNode()->IndexOf(node);
nsCOMPtr<nsINode> parent = node->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 ((!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(node)) &&
(!mHTMLEditor || !mHTMLEditor->IsDescendantOfEditorRoot(parent))) {
NS_ENSURE_TRUE_VOID(mHTMLEditor);
break;
}
node = parent;
// we want to be AFTER nearNode
offset = parentOffset + 1;
NS_ENSURE_TRUE_VOID(mHTMLEditor);
nearNode = mHTMLEditor->GetNextHTMLNode(node, offset, true);
}
*outNode = node->AsDOMNode();
*outOffset = offset;
}
/**
* GetPromotedRanges() runs all the selection range endpoint through
* GetPromotedPoint().
*/
void
HTMLEditRules::GetPromotedRanges(Selection& aSelection,
nsTArray<RefPtr<nsRange>>& outArrayOfRanges,
EditAction inOperationType)
{
uint32_t rangeCount = aSelection.RangeCount();
for (uint32_t i = 0; i < rangeCount; i++) {
RefPtr<nsRange> selectionRange = aSelection.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, inOperationType);
// Stuff new opRange into array
outArrayOfRanges.AppendElement(opRange);
}
}
/**
* PromoteRange() expands a range to include any parents for which all editable
* children are already in range.
*/
void
HTMLEditRules::PromoteRange(nsRange& aRange,
EditAction aOperationType)
{
NS_ENSURE_TRUE(mHTMLEditor, );
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
nsCOMPtr<nsINode> startNode = aRange.GetStartParent();
nsCOMPtr<nsINode> endNode = aRange.GetEndParent();
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) {
nsCOMPtr<Element> block = htmlEditor->GetBlock(*startNode);
if (block) {
bool bIsEmptyNode = false;
nsCOMPtr<nsIContent> root = htmlEditor->GetActiveEditingHost();
// Make sure we don't go higher than our root element in the content tree
NS_ENSURE_TRUE(root, );
if (!nsContentUtils::ContentIsDescendantOf(root, block)) {
htmlEditor->IsEmptyNode(block, &bIsEmptyNode, true, false);
}
if (bIsEmptyNode) {
startNode = block;
endNode = block;
startOffset = 0;
endOffset = block->Length();
}
}
}
// 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.
nsCOMPtr<nsIDOMNode> opStartNode;
nsCOMPtr<nsIDOMNode> opEndNode;
int32_t opStartOffset, opEndOffset;
GetPromotedPoint(kStart, GetAsDOMNode(startNode), startOffset,
aOperationType, address_of(opStartNode), &opStartOffset);
GetPromotedPoint(kEnd, GetAsDOMNode(endNode), endOffset, aOperationType,
address_of(opEndNode), &opEndOffset);
// Make sure that the new range ends up to be in the editable section.
if (!htmlEditor->IsDescendantOfEditorRoot(
EditorBase::GetNodeAtRangeOffsetPoint(opStartNode, opStartOffset)) ||
!htmlEditor->IsDescendantOfEditorRoot(
EditorBase::GetNodeAtRangeOffsetPoint(opEndNode, opEndOffset - 1))) {
return;
}
DebugOnly<nsresult> rv = aRange.SetStart(opStartNode, opStartOffset);
MOZ_ASSERT(NS_SUCCEEDED(rv));
rv = aRange.SetEnd(opEndNode, opEndOffset);
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
{
return !mArray.Contains(aNode);
}
private:
nsTArray<OwningNonNull<nsINode>>& mArray;
};
/**
* GetNodesForOperation() runs through the ranges in the array and construct a
* new array of nodes to be acted on.
*/
nsresult
HTMLEditRules::GetNodesForOperation(
nsTArray<RefPtr<nsRange>>& aArrayOfRanges,
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes,
EditAction aOperationType,
TouchContent aTouchContent)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
int32_t rangeCount = aArrayOfRanges.Length();
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 (int32_t i = 0; i < rangeCount; i++) {
RefPtr<nsRange> r = aArrayOfRanges[i];
nsCOMPtr<nsINode> endParent = r->GetEndParent();
if (!endParent->IsNodeOfType(nsINode::eTEXT)) {
continue;
}
int32_t offset = r->EndOffset();
if (0 < offset &&
offset < static_cast<int32_t>(endParent->Length())) {
// Split the text node.
nsCOMPtr<nsIDOMNode> tempNode;
nsresult rv = htmlEditor->SplitNode(endParent->AsDOMNode(), offset,
getter_AddRefs(tempNode));
NS_ENSURE_SUCCESS(rv, rv);
// Correct the range.
// The new end parent becomes the parent node of the text.
nsCOMPtr<nsIContent> newParent = endParent->GetParent();
r->SetEnd(newParent, newParent->IndexOf(endParent));
}
}
}
// Bust up any inlines that cross our range endpoints, but only if we are
// allowed to touch content.
if (aTouchContent == TouchContent::yes) {
nsTArray<OwningNonNull<RangeItem>> rangeItemArray;
rangeItemArray.AppendElements(rangeCount);
// First register ranges for special editor gravity
for (int32_t i = 0; i < rangeCount; i++) {
rangeItemArray[i] = new RangeItem();
rangeItemArray[i]->StoreRange(aArrayOfRanges[0]);
htmlEditor->mRangeUpdater.RegisterRangeItem(rangeItemArray[i]);
aArrayOfRanges.RemoveElementAt(0);
}
// Now bust up inlines.
nsresult rv = NS_OK;
for (auto& item : Reversed(rangeItemArray)) {
rv = BustUpInlinesAtRangeEndpoints(*item);
if (NS_FAILED(rv)) {
break;
}
}
// Then unregister the ranges
for (auto& item : rangeItemArray) {
htmlEditor->mRangeUpdater.DropRangeItem(item);
aArrayOfRanges.AppendElement(item->GetRange());
}
NS_ENSURE_SUCCESS(rv, rv);
}
// Gather up a list of all the nodes
for (auto& range : aArrayOfRanges) {
DOMSubtreeIterator iter;
nsresult rv = iter.Init(*range);
NS_ENSURE_SUCCESS(rv, 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 (aOperationType == EditAction::makeBasicBlock) {
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);
}
}
}
// 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 (aOperationType == EditAction::outdent ||
aOperationType == EditAction::indent ||
aOperationType == EditAction::setAbsolutePosition) {
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 (aOperationType == EditAction::outdent &&
!htmlEditor->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 (aOperationType == EditAction::makeBasicBlock ||
aOperationType == EditAction::makeList ||
aOperationType == EditAction::align ||
aOperationType == EditAction::setAbsolutePosition ||
aOperationType == EditAction::indent ||
aOperationType == EditAction::outdent) {
for (int32_t i = aOutArrayOfNodes.Length() - 1; i >= 0; i--) {
OwningNonNull<nsINode> node = aOutArrayOfNodes[i];
if (aTouchContent == TouchContent::yes && IsInlineNode(node) &&
htmlEditor->IsContainer(node) && !EditorBase::IsTextNode(node)) {
nsTArray<OwningNonNull<nsINode>> arrayOfInlines;
nsresult rv = BustUpInlinesAtBRs(*node->AsContent(), arrayOfInlines);
NS_ENSURE_SUCCESS(rv, 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)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
RefPtr<Selection> selection = htmlEditor->GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
// 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 = selection->RangeCount();
for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
RefPtr<nsRange> range = selection->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 dontSpazMySelection(htmlEditor);
// contruct a list of nodes to act on.
nsresult rv = GetNodesFromSelection(*selection, EditAction::makeList,
aOutArrayOfNodes, aTouchContent);
NS_ENSURE_SUCCESS(rv, 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 (!htmlEditor->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)
{
NS_ENSURE_TRUE(mHTMLEditor, );
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// 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 = htmlEditor->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,
TouchContent aTouchContent)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
RefPtr<Selection> selection = htmlEditor->GetSelection();
NS_ENSURE_STATE(selection);
// Contruct a list of nodes to act on.
nsresult rv = GetNodesFromSelection(*selection, EditAction::makeBasicBlock,
outArrayOfNodes, aTouchContent);
NS_ENSURE_SUCCESS(rv, 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 (!htmlEditor->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& item)
{
bool isCollapsed = ((item.startNode == item.endNode) && (item.startOffset == item.endOffset));
nsCOMPtr<nsIContent> endInline = GetHighestInlineParent(*item.endNode);
// if we have inline parents above range endpoints, split them
if (endInline && !isCollapsed) {
nsCOMPtr<nsINode> resultEndNode = endInline->GetParentNode();
NS_ENSURE_STATE(mHTMLEditor);
// item.endNode must be content if endInline isn't null
int32_t resultEndOffset =
mHTMLEditor->SplitNodeDeep(*endInline, *item.endNode->AsContent(),
item.endOffset,
EditorBase::EmptyContainers::no);
NS_ENSURE_TRUE(resultEndOffset != -1, NS_ERROR_FAILURE);
// reset range
item.endNode = resultEndNode;
item.endOffset = resultEndOffset;
}
nsCOMPtr<nsIContent> startInline = GetHighestInlineParent(*item.startNode);
if (startInline) {
nsCOMPtr<nsINode> resultStartNode = startInline->GetParentNode();
NS_ENSURE_STATE(mHTMLEditor);
int32_t resultStartOffset =
mHTMLEditor->SplitNodeDeep(*startInline, *item.startNode->AsContent(),
item.startOffset,
EditorBase::EmptyContainers::no);
NS_ENSURE_TRUE(resultStartOffset != -1, NS_ERROR_FAILURE);
// reset range
item.startNode = resultStartNode;
item.startOffset = resultStartOffset;
}
return NS_OK;
}
nsresult
HTMLEditRules::BustUpInlinesAtBRs(
nsIContent& aNode,
nsTArray<OwningNonNull<nsINode>>& aOutArrayOfNodes)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// 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 inNode along all the breaks
nsCOMPtr<nsINode> inlineParentNode = aNode.GetParentNode();
nsCOMPtr<nsIContent> splitDeepNode = &aNode;
nsCOMPtr<nsIContent> leftNode, rightNode;
for (uint32_t i = 0; i < arrayOfBreaks.Length(); i++) {
OwningNonNull<Element> breakNode = *arrayOfBreaks[i]->AsElement();
NS_ENSURE_TRUE(splitDeepNode, NS_ERROR_NULL_POINTER);
NS_ENSURE_TRUE(breakNode->GetParent(), NS_ERROR_NULL_POINTER);
OwningNonNull<nsIContent> splitParentNode = *breakNode->GetParent();
int32_t splitOffset = splitParentNode->IndexOf(breakNode);
int32_t resultOffset =
htmlEditor->SplitNodeDeep(*splitDeepNode, splitParentNode, splitOffset,
HTMLEditor::EmptyContainers::yes,
getter_AddRefs(leftNode),
getter_AddRefs(rightNode));
NS_ENSURE_STATE(resultOffset != -1);
// Put left node in node list
if (leftNode) {
// Might not be a left node. A break might have been at the very
// beginning of inline container, in which case SplitNodeDeep would not
// actually split anything
aOutArrayOfNodes.AppendElement(*leftNode);
}
// Move break outside of container and also put in node list
nsresult rv =
htmlEditor->MoveNode(breakNode, inlineParentNode, resultOffset);
NS_ENSURE_SUCCESS(rv, rv);
aOutArrayOfNodes.AppendElement(*breakNode);
// Now rightNode becomes the new node to split
splitDeepNode = rightNode;
}
// Now tack on remaining rightNode, if any, to the list
if (rightNode) {
aOutArrayOfNodes.AppendElement(*rightNode);
}
return NS_OK;
}
nsIContent*
HTMLEditRules::GetHighestInlineParent(nsINode& aNode)
{
if (!aNode.IsContent() || IsBlockNode(aNode)) {
return nullptr;
}
OwningNonNull<nsIContent> node = *aNode.AsContent();
while (node->GetParent() && IsInlineNode(*node->GetParent())) {
node = *node->GetParent();
}
return node;
}
/**
* GetNodesFromPoint() constructs a list of nodes from a point that will be
* operated on.
*/
nsresult
HTMLEditRules::GetNodesFromPoint(
EditorDOMPoint aPoint,
EditAction aOperation,
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
TouchContent aTouchContent)
{
NS_ENSURE_STATE(aPoint.node);
RefPtr<nsRange> range = new nsRange(aPoint.node);
nsresult rv = range->SetStart(aPoint.node, aPoint.offset);
MOZ_ASSERT(NS_SUCCEEDED(rv));
// Expand the range to include adjacent inlines
PromoteRange(*range, aOperation);
// 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
rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes, aOperation,
aTouchContent);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
/**
* GetNodesFromSelection() constructs a list of nodes from the selection that
* will be operated on.
*/
nsresult
HTMLEditRules::GetNodesFromSelection(
Selection& aSelection,
EditAction aOperation,
nsTArray<OwningNonNull<nsINode>>& outArrayOfNodes,
TouchContent aTouchContent)
{
// Promote selection ranges
nsTArray<RefPtr<nsRange>> arrayOfRanges;
GetPromotedRanges(aSelection, arrayOfRanges, aOperation);
// Use these ranges to contruct a list of nodes to act on.
nsresult rv = GetNodesForOperation(arrayOfRanges, outArrayOfNodes,
aOperation, aTouchContent);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
/**
* MakeTransitionList() detects all the transitions in the array, where a
* transition means that adjacent nodes in the array don't have the same parent.
*/
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();
}
}
/**
* If aNode is the descendant of a listitem, return that li. But table element
* boundaries are stoppers on the search. Also stops on the active editor host
* (contenteditable). Also test if aNode is an li itself.
*/
Element*
HTMLEditRules::IsInListItem(nsINode* aNode)
{
NS_ENSURE_TRUE(aNode, nullptr);
if (HTMLEditUtils::IsListItem(aNode)) {
return aNode->AsElement();
}
Element* parent = aNode->GetParentElement();
while (parent &&
mHTMLEditor && mHTMLEditor->IsDescendantOfEditorRoot(parent) &&
!HTMLEditUtils::IsTableElement(parent)) {
if (HTMLEditUtils::IsListItem(parent)) {
return parent;
}
parent = parent->GetParentElement();
}
return nullptr;
}
/**
* ReturnInHeader: do the right thing for returns pressed in headers
*/
nsresult
HTMLEditRules::ReturnInHeader(Selection& aSelection,
Element& aHeader,
nsINode& aNode,
int32_t aOffset)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// Remember where the header is
nsCOMPtr<nsINode> headerParent = aHeader.GetParentNode();
int32_t offset = headerParent ? headerParent->IndexOf(&aHeader) : -1;
// Get ws code to adjust any ws
nsCOMPtr<nsINode> node = &aNode;
nsresult rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor,
address_of(node),
&aOffset);
NS_ENSURE_SUCCESS(rv, rv);
// Split the header
NS_ENSURE_STATE(node->IsContent());
htmlEditor->SplitNodeDeep(aHeader, *node->AsContent(), aOffset);
// If the left-hand heading is empty, put a mozbr in it
nsCOMPtr<nsIContent> prevItem = htmlEditor->GetPriorHTMLSibling(&aHeader);
if (prevItem && HTMLEditUtils::IsHeader(*prevItem)) {
bool isEmptyNode;
rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode);
NS_ENSURE_SUCCESS(rv, rv);
if (isEmptyNode) {
rv = CreateMozBR(prevItem->AsDOMNode(), 0);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// If the new (righthand) header node is empty, delete it
bool isEmpty;
rv = IsEmptyBlock(aHeader, &isEmpty, MozBRCounts::no);
NS_ENSURE_SUCCESS(rv, rv);
if (isEmpty) {
rv = htmlEditor->DeleteNode(&aHeader);
NS_ENSURE_SUCCESS(rv, rv);
// Layout tells the caret to blink in a weird place if we don't place a
// break after the header.
nsCOMPtr<nsIContent> sibling =
htmlEditor->GetNextHTMLSibling(headerParent, offset + 1);
if (!sibling || !sibling->IsHTMLElement(nsGkAtoms::br)) {
ClearCachedStyles();
htmlEditor->mTypeInState->ClearAllProps();
// Create a paragraph
nsCOMPtr<Element> pNode =
htmlEditor->CreateNode(nsGkAtoms::p, headerParent, offset + 1);
NS_ENSURE_STATE(pNode);
// Append a <br> to it
nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0);
NS_ENSURE_STATE(brNode);
// Set selection to before the break
rv = aSelection.Collapse(pNode, 0);
NS_ENSURE_SUCCESS(rv, rv);
} else {
headerParent = sibling->GetParentNode();
offset = headerParent ? headerParent->IndexOf(sibling) : -1;
// Put selection after break
rv = aSelection.Collapse(headerParent, offset + 1);
NS_ENSURE_SUCCESS(rv, rv);
}
} else {
// Put selection at front of righthand heading
rv = aSelection.Collapse(&aHeader, 0);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
/**
* ReturnInParagraph() does the right thing for returns pressed in paragraphs.
*/
nsresult
HTMLEditRules::ReturnInParagraph(Selection* aSelection,
nsIDOMNode* aPara,
nsIDOMNode* aNode,
int32_t aOffset,
bool* aCancel,
bool* aHandled)
{
nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
if (!aSelection || !aPara || !node || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
*aCancel = false;
*aHandled = false;
int32_t offset;
nsCOMPtr<nsINode> parent = EditorBase::GetNodeLocation(node, &offset);
NS_ENSURE_STATE(mHTMLEditor);
bool doesCRCreateNewP = mHTMLEditor->GetReturnInParagraphCreatesNewParagraph();
bool newBRneeded = false;
bool newSelNode = false;
nsCOMPtr<nsIContent> sibling;
nsCOMPtr<nsIDOMNode> selNode = aNode;
int32_t selOffset = aOffset;
NS_ENSURE_STATE(mHTMLEditor);
if (aNode == aPara && doesCRCreateNewP) {
// we are at the edges of the block, newBRneeded not needed!
sibling = node->AsContent();
} else if (EditorBase::IsTextNode(aNode)) {
nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(aNode);
uint32_t strLength;
nsresult rv = textNode->GetLength(&strLength);
NS_ENSURE_SUCCESS(rv, rv);
// at beginning of text node?
if (!aOffset) {
// is there a BR prior to it?
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetPriorHTMLSibling(node);
if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) ||
TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) {
NS_ENSURE_STATE(mHTMLEditor);
newBRneeded = true;
}
} else if (aOffset == (int32_t)strLength) {
// we're at the end of text node...
// is there a BR after to it?
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->GetNextHTMLSibling(node);
if (!sibling || !mHTMLEditor || !mHTMLEditor->IsVisBreak(sibling) ||
TextEditUtils::HasMozAttr(GetAsDOMNode(sibling))) {
NS_ENSURE_STATE(mHTMLEditor);
newBRneeded = true;
offset++;
}
} else {
if (doesCRCreateNewP) {
nsCOMPtr<nsIDOMNode> tmp;
if (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_UNEXPECTED;
}
rv = mHTMLEditor->SplitNode(aNode, aOffset, getter_AddRefs(tmp));
NS_ENSURE_SUCCESS(rv, rv);
selNode = tmp;
}
newBRneeded = true;
offset++;
}
} else {
// not in a text node.
// is there a BR prior to it?
nsCOMPtr<nsIContent> nearNode;
NS_ENSURE_STATE(mHTMLEditor);
nearNode = mHTMLEditor->GetPriorHTMLNode(node, aOffset);
NS_ENSURE_STATE(mHTMLEditor);
if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) ||
TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) {
// is there a BR after it?
NS_ENSURE_STATE(mHTMLEditor);
nearNode = mHTMLEditor->GetNextHTMLNode(node, aOffset);
NS_ENSURE_STATE(mHTMLEditor);
if (!nearNode || !mHTMLEditor->IsVisBreak(nearNode) ||
TextEditUtils::HasMozAttr(GetAsDOMNode(nearNode))) {
newBRneeded = true;
parent = node;
offset = aOffset;
newSelNode = true;
}
}
if (!newBRneeded) {
sibling = nearNode;
}
}
if (newBRneeded) {
// if CR does not create a new P, default to BR creation
NS_ENSURE_TRUE(doesCRCreateNewP, NS_OK);
NS_ENSURE_STATE(mHTMLEditor);
sibling = mHTMLEditor->CreateBR(parent, offset);
if (newSelNode) {
// We split the parent after the br we've just inserted.
selNode = GetAsDOMNode(parent);
selOffset = offset + 1;
}
}
*aHandled = true;
return SplitParagraph(aPara, sibling, aSelection, address_of(selNode), &selOffset);
}
/**
* SplitParagraph() splits a paragraph at selection point, possibly deleting a
* br.
*/
nsresult
HTMLEditRules::SplitParagraph(nsIDOMNode *aPara,
nsIContent* aBRNode,
Selection* aSelection,
nsCOMPtr<nsIDOMNode>* aSelNode,
int32_t* aOffset)
{
nsCOMPtr<Element> para = do_QueryInterface(aPara);
NS_ENSURE_TRUE(para && aBRNode && aSelNode && *aSelNode && aOffset &&
aSelection, NS_ERROR_NULL_POINTER);
// split para
// get ws code to adjust any ws
nsCOMPtr<nsIContent> leftPara, rightPara;
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsINode> selNode(do_QueryInterface(*aSelNode));
nsresult rv =
WSRunObject::PrepareToSplitAcrossBlocks(mHTMLEditor,
address_of(selNode), aOffset);
// XXX When it fails, why do we need to return selection node? (Why can the
// caller trust the result even when it returns error?)
*aSelNode = GetAsDOMNode(selNode);
NS_ENSURE_SUCCESS(rv, rv);
// split the paragraph
NS_ENSURE_STATE(mHTMLEditor);
NS_ENSURE_STATE(selNode->IsContent());
mHTMLEditor->SplitNodeDeep(*para, *selNode->AsContent(), *aOffset,
HTMLEditor::EmptyContainers::yes,
getter_AddRefs(leftPara),
getter_AddRefs(rightPara));
// get rid of the break, if it is visible (otherwise it may be needed to prevent an empty p)
NS_ENSURE_STATE(mHTMLEditor);
if (mHTMLEditor->IsVisBreak(aBRNode)) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->DeleteNode(aBRNode);
NS_ENSURE_SUCCESS(rv, rv);
}
// remove ID attribute on the paragraph we just created
RefPtr<Element> rightElt = rightPara->AsElement();
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->RemoveAttribute(rightElt, nsGkAtoms::id);
NS_ENSURE_SUCCESS(rv, rv);
// check both halves of para to see if we need mozBR
rv = InsertMozBRIfNeeded(*leftPara);
NS_ENSURE_SUCCESS(rv, rv);
rv = InsertMozBRIfNeeded(*rightPara);
NS_ENSURE_SUCCESS(rv, rv);
// selection to beginning of right hand para;
// look inside any containers that are up front.
nsCOMPtr<nsINode> rightParaNode = do_QueryInterface(rightPara);
NS_ENSURE_STATE(mHTMLEditor && rightParaNode);
nsCOMPtr<nsIDOMNode> child =
GetAsDOMNode(mHTMLEditor->GetLeftmostChild(rightParaNode, true));
if (EditorBase::IsTextNode(child) ||
mHTMLEditor->IsContainer(child)) {
aSelection->Collapse(child,0);
} else {
int32_t offset;
nsCOMPtr<nsIDOMNode> parent = EditorBase::GetNodeLocation(child, &offset);
aSelection->Collapse(parent,offset);
}
return NS_OK;
}
/**
* ReturnInListItem: do the right thing for returns pressed in list items
*/
nsresult
HTMLEditRules::ReturnInListItem(Selection& aSelection,
Element& aListItem,
nsINode& aNode,
int32_t aOffset)
{
MOZ_ASSERT(HTMLEditUtils::IsListItem(&aListItem));
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// Get the item parent and the active editing host.
nsCOMPtr<Element> root = htmlEditor->GetActiveEditingHost();
nsCOMPtr<Element> list = aListItem.GetParentElement();
int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1;
// 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.
bool isEmpty;
nsresult rv = IsEmptyBlock(aListItem, &isEmpty, MozBRCounts::no);
NS_ENSURE_SUCCESS(rv, rv);
if (isEmpty && root != list && mReturnInEmptyLIKillsList) {
// Get the list offset now -- before we might eventually split the list
nsCOMPtr<nsINode> listParent = list->GetParentNode();
int32_t offset = listParent ? listParent->IndexOf(list) : -1;
// Are we the last list item in the list?
if (!htmlEditor->IsLastEditableChild(&aListItem)) {
// We need to split the list!
ErrorResult rv;
htmlEditor->SplitNode(*list, itemOffset, rv);
NS_ENSURE_TRUE(!rv.Failed(), rv.StealNSResult());
}
// Are we in a sublist?
if (HTMLEditUtils::IsList(listParent)) {
// If so, move item out of this list and into the grandparent list
rv = htmlEditor->MoveNode(&aListItem, listParent, offset + 1);
NS_ENSURE_SUCCESS(rv, rv);
rv = aSelection.Collapse(&aListItem, 0);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Otherwise kill this item
rv = htmlEditor->DeleteNode(&aListItem);
NS_ENSURE_SUCCESS(rv, rv);
// Time to insert a paragraph
nsCOMPtr<Element> pNode =
htmlEditor->CreateNode(nsGkAtoms::p, listParent, offset + 1);
NS_ENSURE_STATE(pNode);
// Append a <br> to it
nsCOMPtr<Element> brNode = htmlEditor->CreateBR(pNode, 0);
NS_ENSURE_STATE(brNode);
// Set selection to before the break
rv = aSelection.Collapse(pNode, 0);
NS_ENSURE_SUCCESS(rv, rv);
}
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;
rv = WSRunObject::PrepareToSplitAcrossBlocks(htmlEditor,
address_of(selNode), &aOffset);
NS_ENSURE_SUCCESS(rv, rv);
// Now split list item
NS_ENSURE_STATE(selNode->IsContent());
htmlEditor->SplitNodeDeep(aListItem, *selNode->AsContent(), aOffset);
// 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 = htmlEditor->GetPriorHTMLSibling(&aListItem);
if (prevItem && HTMLEditUtils::IsListItem(prevItem)) {
bool isEmptyNode;
rv = htmlEditor->IsEmptyNode(prevItem, &isEmptyNode);
NS_ENSURE_SUCCESS(rv, rv);
if (isEmptyNode) {
rv = CreateMozBR(prevItem->AsDOMNode(), 0);
NS_ENSURE_SUCCESS(rv, rv);
} else {
rv = htmlEditor->IsEmptyNode(&aListItem, &isEmptyNode, true);
NS_ENSURE_SUCCESS(rv, rv);
if (isEmptyNode) {
nsCOMPtr<nsIAtom> nodeAtom = aListItem.NodeInfo()->NameAtom();
if (nodeAtom == nsGkAtoms::dd || nodeAtom == nsGkAtoms::dt) {
nsCOMPtr<nsINode> list = aListItem.GetParentNode();
int32_t itemOffset = list ? list->IndexOf(&aListItem) : -1;
nsIAtom* listAtom = nodeAtom == nsGkAtoms::dt ? nsGkAtoms::dd
: nsGkAtoms::dt;
nsCOMPtr<Element> newListItem =
htmlEditor->CreateNode(listAtom, list, itemOffset + 1);
NS_ENSURE_STATE(newListItem);
rv = htmlEditor->DeleteNode(&aListItem);
NS_ENSURE_SUCCESS(rv, rv);
rv = aSelection.Collapse(newListItem, 0);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsCOMPtr<Element> brNode;
rv = htmlEditor->CopyLastEditableChildStyles(GetAsDOMNode(prevItem),
GetAsDOMNode(&aListItem),
getter_AddRefs(brNode));
NS_ENSURE_SUCCESS(rv, rv);
if (brNode) {
nsCOMPtr<nsINode> brParent = brNode->GetParentNode();
int32_t offset = brParent ? brParent->IndexOf(brNode) : -1;
rv = aSelection.Collapse(brParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
} else {
WSRunObject wsObj(htmlEditor, &aListItem, 0);
nsCOMPtr<nsINode> visNode;
int32_t visOffset = 0;
WSType wsType;
wsObj.NextVisibleNode(&aListItem, 0, address_of(visNode),
&visOffset, &wsType);
if (wsType == WSType::special || wsType == WSType::br ||
visNode->IsHTMLElement(nsGkAtoms::hr)) {
nsCOMPtr<nsINode> parent = visNode->GetParentNode();
int32_t offset = parent ? parent->IndexOf(visNode) : -1;
rv = aSelection.Collapse(parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
} else {
rv = aSelection.Collapse(visNode, visOffset);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
}
}
}
rv = aSelection.Collapse(&aListItem, 0);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
/**
* MakeBlockquote() puts the list of nodes into one or more blockquotes.
*/
nsresult
HTMLEditRules::MakeBlockquote(nsTArray<OwningNonNull<nsINode>>& aNodeArray)
{
// 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).
nsCOMPtr<Element> curBlock;
nsCOMPtr<nsINode> prevParent;
for (auto& curNode : aNodeArray) {
// Get the node to act on, and its location
NS_ENSURE_STATE(curNode->IsContent());
// 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);
NS_ENSURE_SUCCESS(rv, 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) {
nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
nsresult rv = SplitAsNeeded(*nsGkAtoms::blockquote, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
curBlock = mHTMLEditor->CreateNode(nsGkAtoms::blockquote, curParent,
offset);
NS_ENSURE_STATE(curBlock);
// remember our new block for postprocessing
mNewBlock = curBlock;
// note: doesn't matter if we set mNewBlock multiple times.
}
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->MoveNode(curNode->AsContent(), curBlock, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
/**
* RemoveBlockStyle() makes the nodes have no special block type.
*/
nsresult
HTMLEditRules::RemoveBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// 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 a address, p, header, address, or pre, remove it
if (HTMLEditUtils::IsFormatNode(curNode)) {
// Process any partial progress saved
if (curBlock) {
nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
NS_ENSURE_SUCCESS(rv, rv);
firstNode = lastNode = curBlock = nullptr;
}
// Remove current block
nsresult rv = htmlEditor->RemoveBlockContainer(*curNode->AsContent());
NS_ENSURE_SUCCESS(rv, rv);
} else 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) {
nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
NS_ENSURE_SUCCESS(rv, rv);
firstNode = lastNode = curBlock = nullptr;
}
// Recursion time
nsTArray<OwningNonNull<nsINode>> childArray;
GetChildNodesForOperation(*curNode, childArray);
nsresult rv = RemoveBlockStyle(childArray);
NS_ENSURE_SUCCESS(rv, rv);
} else 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].
nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
NS_ENSURE_SUCCESS(rv, rv);
firstNode = lastNode = curBlock = nullptr;
// Fall out and handle curNode
}
curBlock = htmlEditor->GetBlockNodeParent(curNode);
if (curBlock && HTMLEditUtils::IsFormatNode(curBlock)) {
firstNode = lastNode = curNode->AsContent();
} else {
// Not a block kind that we care about.
curBlock = nullptr;
}
} else if (curBlock) {
// Some node that is already sans block style. Skip over it and process
// any partial progress saved.
nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
NS_ENSURE_SUCCESS(rv, rv);
firstNode = lastNode = curBlock = nullptr;
}
}
// Process any partial progress saved
if (curBlock) {
nsresult rv = RemovePartOfBlock(*curBlock, *firstNode, *lastNode);
NS_ENSURE_SUCCESS(rv, rv);
firstNode = lastNode = curBlock = nullptr;
}
return NS_OK;
}
/**
* ApplyBlockStyle() does whatever it takes to make the list of nodes into one
* or more blocks of type aBlockTag.
*/
nsresult
HTMLEditRules::ApplyBlockStyle(nsTArray<OwningNonNull<nsINode>>& aNodeArray,
nsIAtom& 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...
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// Remove all non-editable nodes. Leave them be.
for (int32_t i = aNodeArray.Length() - 1; i >= 0; i--) {
if (!htmlEditor->IsEditable(aNodeArray[i])) {
aNodeArray.RemoveElementAt(i);
}
}
nsCOMPtr<Element> newBlock;
nsCOMPtr<Element> curBlock;
for (auto& curNode : aNodeArray) {
nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
// Is it already the right kind of block?
if (curNode->IsHTMLElement(&aBlockTag)) {
// 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 = htmlEditor->ReplaceContainer(curNode->AsElement(),
&aBlockTag, nullptr, nullptr,
EditorBase::eCloneAttributes);
NS_ENSURE_STATE(newBlock);
} else 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);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Make sure we can put a block here
nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<Element> theBlock =
htmlEditor->CreateNode(&aBlockTag, curParent, offset);
NS_ENSURE_STATE(theBlock);
// Remember our new block for postprocessing
mNewBlock = theBlock;
}
} else 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 = htmlEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// The break is the first (or even only) node we encountered. Create a
// block for it.
nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset);
NS_ENSURE_STATE(curBlock);
// Remember our new block for postprocessing
mNewBlock = curBlock;
// Note: doesn't matter if we set mNewBlock multiple times.
rv = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
} else 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 && !htmlEditor->IsEditable(curNode)) {
// Do nothing to this block
continue;
}
// If no curBlock, make one
if (!curBlock) {
nsresult rv = SplitAsNeeded(aBlockTag, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
curBlock = htmlEditor->CreateNode(&aBlockTag, curParent, offset);
NS_ENSURE_STATE(curBlock);
// Remember our new block for postprocessing
mNewBlock = curBlock;
// Note: doesn't matter if we set mNewBlock multiple times.
}
// 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 = htmlEditor->MoveNode(curNode->AsContent(), curBlock, -1);
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
/**
* Given a tag name, split inOutParent up to the point where we can insert the
* tag. Adjust inOutParent and inOutOffset to point to new location for tag.
*/
nsresult
HTMLEditRules::SplitAsNeeded(nsIAtom& aTag,
OwningNonNull<nsINode>& aInOutParent,
int32_t& aInOutOffset)
{
// XXX Is there a better way to do this?
nsCOMPtr<nsINode> parent = aInOutParent.forget();
nsresult rv = SplitAsNeeded(aTag, parent, aInOutOffset);
aInOutParent = parent.forget();
return rv;
}
nsresult
HTMLEditRules::SplitAsNeeded(nsIAtom& aTag,
nsCOMPtr<nsINode>& inOutParent,
int32_t& inOutOffset)
{
NS_ENSURE_TRUE(inOutParent, NS_ERROR_NULL_POINTER);
// Check that we have a place that can legally contain the tag
nsCOMPtr<nsINode> tagParent, splitNode;
for (nsCOMPtr<nsINode> parent = inOutParent; parent;
parent = parent->GetParentNode()) {
// Sniffing up the parent tree until we find a legal place for the block
// Don't leave the active editing host
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsDescendantOfEditorRoot(parent)) {
// XXX Why do we need to check mHTMLEditor again here?
NS_ENSURE_STATE(mHTMLEditor);
if (parent != mHTMLEditor->GetActiveEditingHost()) {
return NS_ERROR_FAILURE;
}
}
NS_ENSURE_STATE(mHTMLEditor);
if (mHTMLEditor->CanContainTag(*parent, aTag)) {
// Success
tagParent = parent;
break;
}
splitNode = parent;
}
if (!tagParent) {
// Could not find a place to build tag!
return NS_ERROR_FAILURE;
}
if (splitNode && splitNode->IsContent() && inOutParent->IsContent()) {
// We found a place for block, but above inOutParent. We need to split.
NS_ENSURE_STATE(mHTMLEditor);
int32_t offset = mHTMLEditor->SplitNodeDeep(*splitNode->AsContent(),
*inOutParent->AsContent(),
inOutOffset);
NS_ENSURE_STATE(offset != -1);
inOutParent = tagParent;
inOutOffset = offset;
}
return NS_OK;
}
/**
* JoinNodesSmart: Join two nodes, doing whatever makes sense for their
* children (which often means joining them, too). aNodeLeft & aNodeRight must
* be same type of node.
*
* Returns the point where they're merged, or (nullptr, -1) on failure.
*/
EditorDOMPoint
HTMLEditRules::JoinNodesSmart(nsIContent& aNodeLeft,
nsIContent& aNodeRight)
{
// Caller responsible for left and right node being the same type
nsCOMPtr<nsINode> parent = aNodeLeft.GetParentNode();
NS_ENSURE_TRUE(parent, EditorDOMPoint());
int32_t parOffset = parent->IndexOf(&aNodeLeft);
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) {
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
nsresult rv = mHTMLEditor->MoveNode(&aNodeRight, parent, parOffset);
NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
}
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 = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight);
NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
return ret;
}
// Remember the last left child, and first right child
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
nsCOMPtr<nsIContent> lastLeft = mHTMLEditor->GetLastEditableChild(aNodeLeft);
NS_ENSURE_TRUE(lastLeft, EditorDOMPoint());
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
nsCOMPtr<nsIContent> firstRight = mHTMLEditor->GetFirstEditableChild(aNodeRight);
NS_ENSURE_TRUE(firstRight, EditorDOMPoint());
// For list items, divs, etc., merge smart
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
nsresult rv = mHTMLEditor->JoinNodes(aNodeLeft, aNodeRight);
NS_ENSURE_SUCCESS(rv, EditorDOMPoint());
if (lastLeft && firstRight && mHTMLEditor &&
mHTMLEditor->AreNodesSameType(lastLeft, firstRight) &&
(lastLeft->GetAsText() || !mHTMLEditor ||
(lastLeft->IsElement() && firstRight->IsElement() &&
mHTMLEditor->mCSSEditUtils->ElementsSameStyle(lastLeft->AsElement(),
firstRight->AsElement())))) {
NS_ENSURE_TRUE(mHTMLEditor, EditorDOMPoint());
return JoinNodesSmart(*lastLeft, *firstRight);
}
return ret;
}
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(nsIDOMNode* aNode)
{
NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = GetInlineStyles(aNode, mCachedStyles);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::GetInlineStyles(nsIDOMNode* aNode,
StyleCache aStyleCache[SIZE_STYLE_TABLE])
{
MOZ_ASSERT(aNode);
MOZ_ASSERT(mHTMLEditor);
bool useCSS = mHTMLEditor->IsCSSEnabled();
for (size_t j = 0; j < SIZE_STYLE_TABLE; ++j) {
// If type-in state is set, don't intervene
bool typeInSet, unused;
if (NS_WARN_IF(!mHTMLEditor)) {
return NS_ERROR_UNEXPECTED;
}
mHTMLEditor->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.EqualsLiteral("size"))) {
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->IsTextPropertySetByContent(aNode, aStyleCache[j].tag,
&(aStyleCache[j].attr), nullptr,
isSet, &outValue);
} else {
NS_ENSURE_STATE(mHTMLEditor);
isSet = mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(
aNode, aStyleCache[j].tag, &(aStyleCache[j].attr), outValue,
CSSEditUtils::eComputed);
}
if (isSet) {
aStyleCache[j].mPresent = true;
aStyleCache[j].value.Assign(outValue);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::ReapplyCachedStyles()
{
// 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
NS_ENSURE_STATE(mHTMLEditor);
bool useCSS = mHTMLEditor->IsCSSEnabled();
// get selection point; if it doesn't exist, we have nothing to do
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
if (!selection) {
// If the document is removed from its parent document during executing an
// editor operation with DOMMutationEvent or something, there may be no
// selection.
return NS_OK;
}
if (!selection->RangeCount()) {
// Nothing to do
return NS_OK;
}
nsCOMPtr<nsIContent> selNode =
do_QueryInterface(selection->GetRangeAt(0)->GetStartParent());
if (!selNode) {
// Nothing to do
return NS_OK;
}
StyleCache styleAtInsertionPoint[SIZE_STYLE_TABLE];
InitStyleCacheArray(styleAtInsertionPoint);
nsCOMPtr<nsIDOMNode> selDOMNode = do_QueryInterface(selNode);
MOZ_ASSERT(selDOMNode);
nsresult rv = GetInlineStyles(selDOMNode, styleAtInsertionPoint);
if (NS_WARN_IF(NS_FAILED(rv))) {
return 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
NS_ENSURE_STATE(mHTMLEditor);
bAny = mHTMLEditor->mCSSEditUtils->IsCSSEquivalentToHTMLInlineStyleSet(
selNode, mCachedStyles[i].tag, &(mCachedStyles[i].attr), curValue,
CSSEditUtils::eComputed);
}
if (!bAny) {
// then check typeinstate and html style
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetInlinePropertyBase(*mCachedStyles[i].tag,
&(mCachedStyles[i].attr),
&(mCachedStyles[i].value),
&bFirst, &bAny, &bAll,
&curValue, false);
NS_ENSURE_SUCCESS(rv, 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 || IsStyleCachePreservingAction(mTheAction)) &&
(!styleAtInsertionPoint[i].mPresent ||
styleAtInsertionPoint[i].value != mCachedStyles[i].value)) {
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->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();
}
}
void
HTMLEditRules::AdjustSpecialBreaks()
{
NS_ENSURE_TRUE_VOID(mHTMLEditor);
// Gather list of empty nodes
nsTArray<OwningNonNull<nsINode>> nodeArray;
EmptyEditableFunctor functor(mHTMLEditor);
DOMIterator iter;
if (NS_WARN_IF(NS_FAILED(iter.Init(*mDocChangeRange)))) {
return;
}
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.
nsresult rv = CreateMozBR(node->AsDOMNode(), (int32_t)node->Length());
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
}
}
nsresult
HTMLEditRules::AdjustWhitespace(Selection* aSelection)
{
// get selection point
nsCOMPtr<nsIDOMNode> selNode;
int32_t selOffset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(aSelection,
getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
// ask whitespace object to tweak nbsp's
NS_ENSURE_STATE(mHTMLEditor);
return WSRunObject(mHTMLEditor, selNode, selOffset).AdjustWhitespace();
}
nsresult
HTMLEditRules::PinSelectionToNewBlock(Selection* aSelection)
{
NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
if (!aSelection->Collapsed()) {
return NS_OK;
}
// get the (collapsed) selection location
nsCOMPtr<nsIDOMNode> selNode, temp;
int32_t selOffset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(aSelection,
getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
temp = selNode;
// use ranges and sRangeHelper to compare sel point to new block
nsCOMPtr<nsINode> node = do_QueryInterface(selNode);
NS_ENSURE_STATE(node);
RefPtr<nsRange> range = new nsRange(node);
rv = range->SetStart(selNode, selOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = range->SetEnd(selNode, selOffset);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIContent> block = mNewBlock.get();
NS_ENSURE_TRUE(block, NS_ERROR_NO_INTERFACE);
bool nodeBefore, nodeAfter;
rv = nsRange::CompareNodeToRange(block, range, &nodeBefore, &nodeAfter);
NS_ENSURE_SUCCESS(rv, rv);
if (nodeBefore && nodeAfter) {
return NS_OK; // selection is inside block
} else if (nodeBefore) {
// selection is after block. put at end of block.
nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock);
NS_ENSURE_STATE(mHTMLEditor);
tmp = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*block));
uint32_t endPoint;
if (EditorBase::IsTextNode(tmp) ||
mHTMLEditor->IsContainer(tmp)) {
rv = EditorBase::GetLengthOfDOMNode(tmp, endPoint);
NS_ENSURE_SUCCESS(rv, rv);
} else {
tmp = EditorBase::GetNodeLocation(tmp, (int32_t*)&endPoint);
endPoint++; // want to be after this node
}
return aSelection->Collapse(tmp, (int32_t)endPoint);
} else {
// selection is before block. put at start of block.
nsCOMPtr<nsIDOMNode> tmp = GetAsDOMNode(mNewBlock);
NS_ENSURE_STATE(mHTMLEditor);
tmp = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*block));
int32_t offset;
if (EditorBase::IsTextNode(tmp) ||
mHTMLEditor->IsContainer(tmp)) {
tmp = EditorBase::GetNodeLocation(tmp, &offset);
}
return aSelection->Collapse(tmp, 0);
}
}
void
HTMLEditRules::CheckInterlinePosition(Selection& aSelection)
{
// If the selection isn't collapsed, do nothing.
if (!aSelection.Collapsed()) {
return;
}
NS_ENSURE_TRUE_VOID(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// Get the (collapsed) selection location
NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) &&
aSelection.GetRangeAt(0)->GetStartParent());
OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent();
int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset();
// 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 =
htmlEditor->GetPriorHTMLNode(selNode, selOffset, true);
if (node && node->IsHTMLElement(nsGkAtoms::br)) {
aSelection.SetInterlinePosition(true);
return;
}
// Are we after a block? If so try set caret to following content
node = htmlEditor->GetPriorHTMLSibling(selNode, selOffset);
if (node && IsBlockNode(*node)) {
aSelection.SetInterlinePosition(true);
return;
}
// Are we before a block? If so try set caret to prior content
node = htmlEditor->GetNextHTMLSibling(selNode, selOffset);
if (node && IsBlockNode(*node)) {
aSelection.SetInterlinePosition(false);
}
}
nsresult
HTMLEditRules::AdjustSelection(Selection* aSelection,
nsIEditor::EDirection aAction)
{
NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
// 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 (!aSelection->Collapsed()) {
return NS_OK;
}
// get the (collapsed) selection location
nsCOMPtr<nsINode> selNode, temp;
int32_t selOffset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(aSelection,
getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
temp = selNode;
// are we in an editable node?
NS_ENSURE_STATE(mHTMLEditor);
while (!mHTMLEditor->IsEditable(selNode)) {
// scan up the tree until we find an editable place to be
selNode = EditorBase::GetNodeLocation(temp, &selOffset);
NS_ENSURE_TRUE(selNode, NS_ERROR_FAILURE);
temp = selNode;
NS_ENSURE_STATE(mHTMLEditor);
}
// 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.
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> theblock = mHTMLEditor->GetBlock(*selNode);
if (theblock && mHTMLEditor->IsEditable(theblock)) {
bool bIsEmptyNode;
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->IsEmptyNode(theblock, &bIsEmptyNode, false, false);
NS_ENSURE_SUCCESS(rv, rv);
// check if br can go into the destination node
NS_ENSURE_STATE(mHTMLEditor);
if (bIsEmptyNode && mHTMLEditor->CanContainTag(*selNode, *nsGkAtoms::br)) {
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> rootNode = mHTMLEditor->GetRoot();
NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
if (selNode == rootNode) {
// 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
return CreateMozBR(GetAsDOMNode(selNode), selOffset);
}
}
// are we in a text node?
if (EditorBase::IsTextNode(selNode)) {
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
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIContent> nearNode =
mHTMLEditor->GetPriorHTMLNode(selNode, selOffset);
if (nearNode) {
// is nearNode also a descendant of same block?
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*selNode);
nsCOMPtr<Element> nearBlock = mHTMLEditor->GetBlockNodeParent(nearNode);
if (block && block == nearBlock) {
if (nearNode && TextEditUtils::IsBreak(nearNode)) {
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsVisBreak(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.
nsCOMPtr<nsIDOMNode> brNode;
rv = CreateMozBR(GetAsDOMNode(selNode), selOffset,
getter_AddRefs(brNode));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIDOMNode> brParent =
EditorBase::GetNodeLocation(brNode, &selOffset);
// selection stays *before* moz-br, sticking to it
aSelection->SetInterlinePosition(true);
rv = aSelection->Collapse(brParent, selOffset);
NS_ENSURE_SUCCESS(rv, rv);
} else {
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIContent> nextNode =
mHTMLEditor->GetNextHTMLNode(nearNode, true);
if (nextNode && TextEditUtils::IsMozBR(nextNode)) {
// selection between br and mozbr. make it stick to mozbr
// so that it will be on blank line.
aSelection->SetInterlinePosition(true);
}
}
}
}
}
// we aren't in a textnode: are we adjacent to text or a break or an image?
NS_ENSURE_STATE(mHTMLEditor);
nearNode = mHTMLEditor->GetPriorHTMLNode(selNode, selOffset, true);
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;
}
NS_ENSURE_STATE(mHTMLEditor);
nearNode = mHTMLEditor->GetNextHTMLNode(selNode, selOffset, true);
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.
nsCOMPtr<nsIDOMNode> nearNodeDOM = GetAsDOMNode(nearNode);
rv = FindNearSelectableNode(GetAsDOMNode(selNode), selOffset, aAction,
address_of(nearNodeDOM));
NS_ENSURE_SUCCESS(rv, rv);
nearNode = do_QueryInterface(nearNodeDOM);
if (!nearNode) {
return NS_OK;
}
EditorDOMPoint pt = GetGoodSelPointForNode(*nearNode, aAction);
rv = aSelection->Collapse(pt.node, pt.offset);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
HTMLEditRules::FindNearSelectableNode(nsIDOMNode* aSelNode,
int32_t aSelOffset,
nsIEditor::EDirection& aDirection,
nsCOMPtr<nsIDOMNode>* outSelectableNode)
{
NS_ENSURE_TRUE(aSelNode && outSelectableNode, NS_ERROR_NULL_POINTER);
*outSelectableNode = nullptr;
nsCOMPtr<nsIDOMNode> nearNode, curNode;
if (aDirection == nsIEditor::ePrevious) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, address_of(nearNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, address_of(nearNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// Try the other direction then.
if (!nearNode) {
if (aDirection == nsIEditor::ePrevious) {
aDirection = nsIEditor::eNext;
} else {
aDirection = nsIEditor::ePrevious;
}
if (aDirection == nsIEditor::ePrevious) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset,
address_of(nearNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset,
address_of(nearNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
// scan in the right direction until we find an eligible text node,
// but don't cross any breaks, images, or table elements.
while (nearNode && !(EditorBase::IsTextNode(nearNode) ||
TextEditUtils::IsBreak(nearNode) ||
HTMLEditUtils::IsImage(nearNode))) {
curNode = nearNode;
if (aDirection == nsIEditor::ePrevious) {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetPriorHTMLNode(curNode, address_of(nearNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->GetNextHTMLNode(curNode, address_of(nearNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
NS_ENSURE_STATE(mHTMLEditor);
}
if (nearNode) {
// don't cross any table elements
if (InDifferentTableElements(nearNode, aSelNode)) {
return NS_OK;
}
// otherwise, ok, we have found a good spot to put the selection
*outSelectableNode = do_QueryInterface(nearNode);
}
return NS_OK;
}
bool
HTMLEditRules::InDifferentTableElements(nsIDOMNode* aNode1,
nsIDOMNode* aNode2)
{
nsCOMPtr<nsINode> node1 = do_QueryInterface(aNode1);
nsCOMPtr<nsINode> node2 = do_QueryInterface(aNode2);
return InDifferentTableElements(node1, node2);
}
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::RemoveEmptyNodes()
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
// 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);
NS_ENSURE_SUCCESS(rv, 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 bIsCandidate = false;
bool bIsEmptyNode = false;
bool bIsMailCite = false;
if (node->IsElement()) {
if (node->IsHTMLElement(nsGkAtoms::body)) {
// Don't delete the body
} else if ((bIsMailCite = 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
bIsCandidate = 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 bIsSelInNode;
rv = SelectionEndpointInNode(node, &bIsSelInNode);
NS_ENSURE_SUCCESS(rv, rv);
if (!bIsSelInNode) {
bIsCandidate = true;
}
}
}
if (bIsCandidate) {
// We delete mailcites even if they have a solo br in them. Other
// nodes we require to be empty.
rv = htmlEditor->IsEmptyNode(node->AsDOMNode(), &bIsEmptyNode,
bIsMailCite, true);
NS_ENSURE_SUCCESS(rv, rv);
if (bIsEmptyNode) {
if (bIsMailCite) {
// mailcites go on a separate list from other empty nodes
arrayOfEmptyCites.AppendElement(*node);
} else {
arrayOfEmptyNodes.AppendElement(*node);
}
}
}
if (!bIsEmptyNode && parent) {
// put parent on skip list
skipList.AppendElement(*parent);
}
}
iter->Next();
}
// now delete the empty nodes
for (auto& delNode : arrayOfEmptyNodes) {
if (htmlEditor->IsModifiableNode(delNode)) {
rv = htmlEditor->DeleteNode(delNode);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// Now delete the empty mailcites. This is a separate step because we want
// to pull out any br's and preserve them.
for (auto& delNode : arrayOfEmptyCites) {
bool bIsEmptyNode;
rv = htmlEditor->IsEmptyNode(delNode, &bIsEmptyNode, false, true);
NS_ENSURE_SUCCESS(rv, rv);
if (!bIsEmptyNode) {
// We are deleting a cite that has just a br. We want to delete cite,
// but preserve br.
nsCOMPtr<nsINode> parent = delNode->GetParentNode();
int32_t offset = parent ? parent->IndexOf(delNode) : -1;
nsCOMPtr<Element> br = htmlEditor->CreateBR(parent, offset);
NS_ENSURE_STATE(br);
}
rv = htmlEditor->DeleteNode(delNode);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult
HTMLEditRules::SelectionEndpointInNode(nsINode* aNode,
bool* aResult)
{
NS_ENSURE_TRUE(aNode && aResult, NS_ERROR_NULL_POINTER);
nsIDOMNode* node = aNode->AsDOMNode();
*aResult = false;
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
NS_ENSURE_STATE(selection);
uint32_t rangeCount = selection->RangeCount();
for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
nsCOMPtr<nsIDOMNode> startParent, endParent;
range->GetStartContainer(getter_AddRefs(startParent));
if (startParent) {
if (node == startParent) {
*aResult = true;
return NS_OK;
}
if (EditorUtils::IsDescendantOf(startParent, node)) {
*aResult = true;
return NS_OK;
}
}
range->GetEndContainer(getter_AddRefs(endParent));
if (startParent == endParent) {
continue;
}
if (endParent) {
if (node == endParent) {
*aResult = true;
return NS_OK;
}
if (EditorUtils::IsDescendantOf(endParent, node)) {
*aResult = true;
return NS_OK;
}
}
}
return NS_OK;
}
/**
* IsEmptyInline: Return true if aNode is an empty inline container
*/
bool
HTMLEditRules::IsEmptyInline(nsINode& aNode)
{
NS_ENSURE_TRUE(mHTMLEditor, false);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
if (IsInlineNode(aNode) && htmlEditor->IsContainer(&aNode)) {
bool isEmpty = true;
htmlEditor->IsEmptyNode(&aNode, &isEmpty);
return isEmpty;
}
return false;
}
bool
HTMLEditRules::ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes)
{
// 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.
NS_ENSURE_TRUE(aArrayOfNodes.Length(), true);
NS_ENSURE_TRUE(mHTMLEditor, false);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
int32_t brCount = 0;
for (auto& node : aArrayOfNodes) {
if (!htmlEditor->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)
{
// init out params
if (aOutOfList) {
*aOutOfList = false;
}
nsCOMPtr<nsIContent> kungFuDeathGrip(&aListItem);
Unused << kungFuDeathGrip;
nsCOMPtr<nsINode> curParent = aListItem.GetParentNode();
if (NS_WARN_IF(!curParent)) {
return NS_ERROR_FAILURE;
}
int32_t offset = curParent->IndexOf(&aListItem);
if (!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.
nsCOMPtr<nsINode> curParPar = curParent->GetParentNode();
int32_t parOffset = curParPar ? curParPar->IndexOf(curParent) : -1;
NS_ENSURE_STATE(mHTMLEditor);
bool bIsFirstListItem = mHTMLEditor->IsFirstEditableChild(&aListItem);
NS_ENSURE_STATE(mHTMLEditor);
bool bIsLastListItem = mHTMLEditor->IsLastEditableChild(&aListItem);
if (!bIsFirstListItem && !bIsLastListItem) {
// split the list
ErrorResult rv;
NS_ENSURE_STATE(mHTMLEditor);
mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv);
if (NS_WARN_IF(rv.Failed())) {
return rv.StealNSResult();
}
}
if (!bIsFirstListItem) {
parOffset++;
}
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->MoveNode(&aListItem, curParPar, parOffset);
NS_ENSURE_SUCCESS(rv, rv);
// unwrap list item contents if they are no longer in a list
if (!HTMLEditUtils::IsList(curParPar) &&
HTMLEditUtils::IsListItem(&aListItem)) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->RemoveBlockContainer(*aListItem.AsElement());
NS_ENSURE_SUCCESS(rv, rv);
if (aOutOfList) {
*aOutOfList = true;
}
}
return NS_OK;
}
nsresult
HTMLEditRules::RemoveListStructure(Element& aList)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
while (aList.GetFirstChild()) {
OwningNonNull<nsIContent> child = *aList.GetFirstChild();
if (HTMLEditUtils::IsListItem(child)) {
bool isOutOfList;
// Keep popping it out until it's not in a list anymore
do {
nsresult rv = PopListItem(child, &isOutOfList);
NS_ENSURE_SUCCESS(rv, rv);
} while (!isOutOfList);
} else if (HTMLEditUtils::IsList(child)) {
nsresult rv = RemoveListStructure(*child->AsElement());
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Delete any non-list items for now
nsresult rv = htmlEditor->DeleteNode(child);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// Delete the now-empty list
nsresult rv = htmlEditor->RemoveBlockContainer(aList);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
HTMLEditRules::ConfirmSelectionInBody()
{
// get the body
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mHTMLEditor->GetRoot());
NS_ENSURE_TRUE(rootElement, NS_ERROR_UNEXPECTED);
// get the selection
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
NS_ENSURE_STATE(selection);
// get the selection start location
nsCOMPtr<nsIDOMNode> selNode, temp, parent;
int32_t selOffset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(selection,
getter_AddRefs(selNode), &selOffset);
if (NS_FAILED(rv)) {
return rv;
}
temp = selNode;
// check that selNode is inside body
while (temp && !TextEditUtils::IsBody(temp)) {
temp->GetParentNode(getter_AddRefs(parent));
temp = parent;
}
// if we aren't in the body, force the issue
if (!temp) {
// uncomment this to see when we get bad selections
// NS_NOTREACHED("selection not in body");
selection->Collapse(rootElement, 0);
}
// get the selection end location
rv = EditorBase::GetEndNodeAndOffset(selection,
getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
temp = selNode;
// check that selNode is inside body
while (temp && !TextEditUtils::IsBody(temp)) {
rv = temp->GetParentNode(getter_AddRefs(parent));
temp = parent;
}
// if we aren't in the body, force the issue
if (!temp) {
// uncomment this to see when we get bad selections
// NS_NOTREACHED("selection not in body");
selection->Collapse(rootElement, 0);
}
// XXX This is the result of the last call of GetParentNode(), it doesn't
// make sense...
return rv;
}
nsresult
HTMLEditRules::UpdateDocChangeRange(nsRange* aRange)
{
// 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)
nsCOMPtr<nsIDOMNode> startNode;
nsresult rv = aRange->GetStartContainer(getter_AddRefs(startNode));
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
if (!mHTMLEditor->IsDescendantOfRoot(startNode)) {
// just return - we don't need to adjust mDocChangeRange in this case
return NS_OK;
}
if (!mDocChangeRange) {
// clone aRange.
mDocChangeRange = aRange->CloneRange();
} else {
int16_t result;
// compare starts of ranges
rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START,
aRange, &result);
if (rv == 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;
rv = NS_OK;
}
NS_ENSURE_SUCCESS(rv, rv);
// Positive result means mDocChangeRange start is after aRange start.
if (result > 0) {
int32_t startOffset;
rv = aRange->GetStartOffset(&startOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mDocChangeRange->SetStart(startNode, startOffset);
NS_ENSURE_SUCCESS(rv, rv);
}
// compare ends of ranges
rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END,
aRange, &result);
NS_ENSURE_SUCCESS(rv, rv);
// Negative result means mDocChangeRange end is before aRange end.
if (result < 0) {
nsCOMPtr<nsIDOMNode> endNode;
int32_t endOffset;
rv = aRange->GetEndContainer(getter_AddRefs(endNode));
NS_ENSURE_SUCCESS(rv, rv);
rv = aRange->GetEndOffset(&endOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mDocChangeRange->SetEnd(endNode, endOffset);
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::InsertMozBRIfNeeded(nsINode& aNode)
{
if (!IsBlockNode(aNode)) {
return NS_OK;
}
bool isEmpty;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->IsEmptyNode(&aNode, &isEmpty);
NS_ENSURE_SUCCESS(rv, rv);
if (!isEmpty) {
return NS_OK;
}
return CreateMozBR(aNode.AsDOMNode(), 0);
}
NS_IMETHODIMP
HTMLEditRules::WillCreateNode(const nsAString& aTag,
nsIDOMNode* aParent,
int32_t aPosition)
{
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::DidCreateNode(const nsAString& aTag,
nsIDOMNode* aNode,
nsIDOMNode* aParent,
int32_t aPosition,
nsresult aResult)
{
if (!mListenerEnabled) {
return NS_OK;
}
// assumption that Join keeps the righthand node
nsresult rv = mUtilRange->SelectNode(aNode);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::WillInsertNode(nsIDOMNode* aNode,
nsIDOMNode* aParent,
int32_t aPosition)
{
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::DidInsertNode(nsIDOMNode* aNode,
nsIDOMNode* aParent,
int32_t aPosition,
nsresult aResult)
{
if (!mListenerEnabled) {
return NS_OK;
}
nsresult rv = mUtilRange->SelectNode(aNode);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::WillDeleteNode(nsIDOMNode* aChild)
{
if (!mListenerEnabled) {
return NS_OK;
}
nsresult rv = mUtilRange->SelectNode(aChild);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::DidDeleteNode(nsIDOMNode* aChild,
nsresult aResult)
{
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::WillSplitNode(nsIDOMNode* aExistingRightNode,
int32_t aOffset)
{
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::DidSplitNode(nsIDOMNode* aExistingRightNode,
int32_t aOffset,
nsIDOMNode* aNewLeftNode,
nsresult aResult)
{
if (!mListenerEnabled) {
return NS_OK;
}
nsresult rv = mUtilRange->SetStart(aNewLeftNode, 0);
NS_ENSURE_SUCCESS(rv, rv);
rv = mUtilRange->SetEnd(aExistingRightNode, 0);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::WillJoinNodes(nsIDOMNode* aLeftNode,
nsIDOMNode* aRightNode,
nsIDOMNode* aParent)
{
if (!mListenerEnabled) {
return NS_OK;
}
// remember split point
return EditorBase::GetLengthOfDOMNode(aLeftNode, mJoinOffset);
}
NS_IMETHODIMP
HTMLEditRules::DidJoinNodes(nsIDOMNode* aLeftNode,
nsIDOMNode* aRightNode,
nsIDOMNode* aParent,
nsresult aResult)
{
if (!mListenerEnabled) {
return NS_OK;
}
// assumption that Join keeps the righthand node
nsresult rv = mUtilRange->SetStart(aRightNode, mJoinOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mUtilRange->SetEnd(aRightNode, mJoinOffset);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::WillInsertText(nsIDOMCharacterData* aTextNode,
int32_t aOffset,
const nsAString& aString)
{
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::DidInsertText(nsIDOMCharacterData* aTextNode,
int32_t aOffset,
const nsAString& aString,
nsresult aResult)
{
if (!mListenerEnabled) {
return NS_OK;
}
int32_t length = aString.Length();
nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode);
nsresult rv = mUtilRange->SetStart(theNode, aOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mUtilRange->SetEnd(theNode, aOffset+length);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::WillDeleteText(nsIDOMCharacterData* aTextNode,
int32_t aOffset,
int32_t aLength)
{
return NS_OK;
}
NS_IMETHODIMP
HTMLEditRules::DidDeleteText(nsIDOMCharacterData* aTextNode,
int32_t aOffset,
int32_t aLength,
nsresult aResult)
{
if (!mListenerEnabled) {
return NS_OK;
}
nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode);
nsresult rv = mUtilRange->SetStart(theNode, aOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mUtilRange->SetEnd(theNode, aOffset);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::WillDeleteSelection(nsISelection* aSelection)
{
if (!mListenerEnabled) {
return NS_OK;
}
if (NS_WARN_IF(!aSelection)) {
return NS_ERROR_INVALID_ARG;
}
RefPtr<Selection> selection = aSelection->AsSelection();
// get the (collapsed) selection location
nsCOMPtr<nsIDOMNode> selNode;
int32_t selOffset;
nsresult rv =
EditorBase::GetStartNodeAndOffset(selection,
getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mUtilRange->SetStart(selNode, selOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = EditorBase::GetEndNodeAndOffset(selection,
getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = mUtilRange->SetEnd(selNode, selOffset);
NS_ENSURE_SUCCESS(rv, rv);
return UpdateDocChangeRange(mUtilRange);
}
NS_IMETHODIMP
HTMLEditRules::DidDeleteSelection(nsISelection *aSelection)
{
return NS_OK;
}
// 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.
nsresult
HTMLEditRules::RemoveAlignment(nsIDOMNode* aNode,
const nsAString& aAlignType,
bool aChildrenOnly)
{
NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
if (EditorBase::IsTextNode(aNode) || HTMLEditUtils::IsTable(aNode)) {
return NS_OK;
}
nsCOMPtr<nsIDOMNode> child = aNode,tmp;
if (aChildrenOnly) {
aNode->GetFirstChild(getter_AddRefs(child));
}
NS_ENSURE_STATE(mHTMLEditor);
bool useCSS = mHTMLEditor->IsCSSEnabled();
while (child) {
if (aChildrenOnly) {
// get the next sibling right now because we could have to remove child
child->GetNextSibling(getter_AddRefs(tmp));
} else {
tmp = nullptr;
}
bool isBlock;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isBlock);
NS_ENSURE_SUCCESS(rv, rv);
if (EditorBase::NodeIsType(child, nsGkAtoms::center)) {
// the current node is a CENTER element
// first remove children's alignment
rv = RemoveAlignment(child, aAlignType, true);
NS_ENSURE_SUCCESS(rv, 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 = MakeSureElemStartsOrEndsOnCR(child);
NS_ENSURE_SUCCESS(rv, rv);
// now remove the CENTER container
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<Element> childAsElement = do_QueryInterface(child);
NS_ENSURE_STATE(childAsElement);
rv = mHTMLEditor->RemoveContainer(childAsElement);
NS_ENSURE_SUCCESS(rv, rv);
} else if (isBlock || HTMLEditUtils::IsHR(child)) {
// the current node is a block element
nsCOMPtr<Element> curElem = do_QueryInterface(child);
if (HTMLEditUtils::SupportsAlignAttr(child)) {
// remove the ALIGN attribute if this element can have it
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->RemoveAttribute(curElem, nsGkAtoms::align);
NS_ENSURE_SUCCESS(rv, rv);
}
if (useCSS) {
if (HTMLEditUtils::IsTable(child) || HTMLEditUtils::IsHR(child)) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->SetAttributeOrEquivalent(curElem,
nsGkAtoms::align,
aAlignType, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
nsAutoString dummyCssValue;
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->mCSSEditUtils->RemoveCSSInlineStyle(
child,
nsGkAtoms::textAlign,
dummyCssValue);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
if (!HTMLEditUtils::IsTable(child)) {
// unless this is a table, look at children
rv = RemoveAlignment(child, aAlignType, true);
NS_ENSURE_SUCCESS(rv, rv);
}
}
child = tmp;
}
return NS_OK;
}
// Let's insert a BR as first (resp. last) child of aNode if its
// first (resp. last) child is not a block nor a BR, and if the
// previous (resp. next) sibling is not a block nor a BR
nsresult
HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode,
bool aStarts)
{
nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
NS_ENSURE_TRUE(node, NS_ERROR_NULL_POINTER);
nsCOMPtr<nsIDOMNode> child;
if (aStarts) {
NS_ENSURE_STATE(mHTMLEditor);
child = GetAsDOMNode(mHTMLEditor->GetFirstEditableChild(*node));
} else {
NS_ENSURE_STATE(mHTMLEditor);
child = GetAsDOMNode(mHTMLEditor->GetLastEditableChild(*node));
}
NS_ENSURE_TRUE(child, NS_OK);
bool isChildBlock;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv = mHTMLEditor->NodeIsBlockStatic(child, &isChildBlock);
NS_ENSURE_SUCCESS(rv, rv);
bool foundCR = false;
if (isChildBlock || TextEditUtils::IsBreak(child)) {
foundCR = true;
} else {
nsCOMPtr<nsIDOMNode> sibling;
if (aStarts) {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->GetPriorHTMLSibling(aNode, address_of(sibling));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else {
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->GetNextHTMLSibling(aNode, address_of(sibling));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (sibling) {
bool isBlock;
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->NodeIsBlockStatic(sibling, &isBlock);
NS_ENSURE_SUCCESS(rv, rv);
if (isBlock || TextEditUtils::IsBreak(sibling)) {
foundCR = true;
}
} else {
foundCR = true;
}
}
if (!foundCR) {
int32_t offset = 0;
if (!aStarts) {
nsCOMPtr<nsINode> node = do_QueryInterface(aNode);
NS_ENSURE_STATE(node);
offset = node->GetChildCount();
}
nsCOMPtr<nsIDOMNode> brNode;
NS_ENSURE_STATE(mHTMLEditor);
rv = mHTMLEditor->CreateBR(aNode, offset, address_of(brNode));
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult
HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsIDOMNode* aNode)
{
nsresult rv = MakeSureElemStartsOrEndsOnCR(aNode, false);
NS_ENSURE_SUCCESS(rv, rv);
return MakeSureElemStartsOrEndsOnCR(aNode, true);
}
nsresult
HTMLEditRules::AlignBlock(Element& aElement,
const nsAString& aAlignType,
ContentsOnly aContentsOnly)
{
if (!IsBlockNode(aElement) && !aElement.IsHTMLElement(nsGkAtoms::hr)) {
// We deal only with blocks; early way out
return NS_OK;
}
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
nsresult rv = RemoveAlignment(aElement.AsDOMNode(), aAlignType,
aContentsOnly == ContentsOnly::yes);
NS_ENSURE_SUCCESS(rv, rv);
if (htmlEditor->IsCSSEnabled()) {
// Let's use CSS alignment; we use margin-left and margin-right for tables
// and text-align for other block-level elements
rv = htmlEditor->SetAttributeOrEquivalent(
&aElement, nsGkAtoms::align, aAlignType, false);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// HTML case; this code is supposed to be called ONLY if the element
// supports the align attribute but we'll never know...
if (HTMLEditUtils::SupportsAlignAttr(aElement.AsDOMNode())) {
rv = htmlEditor->SetAttribute(&aElement, nsGkAtoms::align, aAlignType);
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
nsresult
HTMLEditRules::ChangeIndentation(Element& aElement,
Change aChange)
{
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
nsIAtom& marginProperty =
MarginPropertyAtomForIndent(*htmlEditor->mCSSEditUtils, aElement);
nsAutoString value;
htmlEditor->mCSSEditUtils->GetSpecifiedProperty(aElement, marginProperty,
value);
float f;
nsCOMPtr<nsIAtom> unit;
htmlEditor->mCSSEditUtils->ParseLength(value, &f, getter_AddRefs(unit));
if (!f) {
nsAutoString defaultLengthUnit;
htmlEditor->mCSSEditUtils->GetDefaultLengthUnit(defaultLengthUnit);
unit = NS_Atomize(defaultLengthUnit);
}
int8_t multiplier = aChange == Change::plus ? +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));
htmlEditor->mCSSEditUtils->SetCSSProperty(aElement, marginProperty,
newValue);
return NS_OK;
}
htmlEditor->mCSSEditUtils->RemoveCSSProperty(aElement, marginProperty,
value);
// Remove unnecessary divs
if (!aElement.IsHTMLElement(nsGkAtoms::div) ||
&aElement == htmlEditor->GetActiveEditingHost() ||
!htmlEditor->IsDescendantOfEditorRoot(&aElement) ||
HTMLEditor::HasAttributes(&aElement)) {
return NS_OK;
}
nsresult rv = htmlEditor->RemoveContainer(&aElement);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
HTMLEditRules::WillAbsolutePosition(Selection& aSelection,
bool* aCancel,
bool* aHandled)
{
MOZ_ASSERT(aCancel && aHandled);
NS_ENSURE_STATE(mHTMLEditor);
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
WillInsert(aSelection, aCancel);
// We want to ignore result of WillInsert()
*aCancel = false;
*aHandled = true;
nsCOMPtr<Element> focusElement = htmlEditor->GetSelectionContainer();
if (focusElement && HTMLEditUtils::IsImage(focusElement)) {
mNewBlock = focusElement;
return NS_OK;
}
nsresult rv = NormalizeSelection(&aSelection);
NS_ENSURE_SUCCESS(rv, rv);
AutoSelectionRestorer selectionRestorer(&aSelection, htmlEditor);
// 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(aSelection, arrayOfRanges,
EditAction::setAbsolutePosition);
// Use these ranges to contruct a list of nodes to act on.
nsTArray<OwningNonNull<nsINode>> arrayOfNodes;
rv = GetNodesForOperation(arrayOfRanges, arrayOfNodes,
EditAction::setAbsolutePosition);
NS_ENSURE_SUCCESS(rv, rv);
// If nothing visible in list, make an empty block
if (ListIsEmptyLine(arrayOfNodes)) {
// Get selection location
NS_ENSURE_STATE(aSelection.GetRangeAt(0) &&
aSelection.GetRangeAt(0)->GetStartParent());
OwningNonNull<nsINode> parent =
*aSelection.GetRangeAt(0)->GetStartParent();
int32_t offset = aSelection.GetRangeAt(0)->StartOffset();
// Make sure we can put a block here
rv = SplitAsNeeded(*nsGkAtoms::div, parent, offset);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<Element> positionedDiv =
htmlEditor->CreateNode(nsGkAtoms::div, parent, offset);
NS_ENSURE_STATE(positionedDiv);
// Remember our new block for postprocessing
mNewBlock = positionedDiv;
// Delete anything that was in the list of nodes
while (!arrayOfNodes.IsEmpty()) {
OwningNonNull<nsINode> curNode = arrayOfNodes[0];
rv = htmlEditor->DeleteNode(curNode);
NS_ENSURE_SUCCESS(rv, rv);
arrayOfNodes.RemoveElementAt(0);
}
// Put selection in new block
*aHandled = true;
rv = aSelection.Collapse(positionedDiv, 0);
// Don't restore the selection
selectionRestorer.Abort();
NS_ENSURE_SUCCESS(rv, rv);
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 (uint32_t i = 0; i < arrayOfNodes.Length(); i++) {
// Here's where we actually figure out what to do
NS_ENSURE_STATE(arrayOfNodes[i]->IsContent());
OwningNonNull<nsIContent> curNode = *arrayOfNodes[i]->AsContent();
// Ignore all non-editable nodes. Leave them be.
if (!htmlEditor->IsEditable(curNode)) {
continue;
}
nsCOMPtr<nsIContent> sibling;
nsCOMPtr<nsINode> curParent = curNode->GetParentNode();
int32_t offset = curParent ? curParent->IndexOf(curNode) : -1;
// Some logic for putting list items into nested lists...
if (HTMLEditUtils::IsList(curParent)) {
// 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 = htmlEditor->GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
// Create a new nested list of correct type
rv =
SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
if (!curPositionedDiv) {
nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode();
int32_t parentOffset = curParentParent
? curParentParent->IndexOf(curParent) : -1;
curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParentParent,
parentOffset);
mNewBlock = curPositionedDiv;
}
curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
curPositionedDiv, -1);
NS_ENSURE_STATE(curList);
// 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 = htmlEditor->MoveNode(curNode, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// 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.
nsCOMPtr<Element> listItem = IsInListItem(curNode);
if (listItem) {
if (indentedLI == listItem) {
// Already indented this list item
continue;
}
curParent = listItem->GetParentNode();
offset = curParent ? curParent->IndexOf(listItem) : -1;
// 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 = htmlEditor->GetPriorHTMLSibling(curNode);
}
if (!curList || (sibling && sibling != curList)) {
// Create a new nested list of correct type
rv = SplitAsNeeded(*curParent->NodeInfo()->NameAtom(), curParent,
offset);
NS_ENSURE_SUCCESS(rv, rv);
if (!curPositionedDiv) {
nsCOMPtr<nsINode> curParentParent = curParent->GetParentNode();
int32_t parentOffset = curParentParent ?
curParentParent->IndexOf(curParent) : -1;
curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div,
curParentParent,
parentOffset);
mNewBlock = curPositionedDiv;
}
curList = htmlEditor->CreateNode(curParent->NodeInfo()->NameAtom(),
curPositionedDiv, -1);
NS_ENSURE_STATE(curList);
}
rv = htmlEditor->MoveNode(listItem, curList, -1);
NS_ENSURE_SUCCESS(rv, rv);
// Remember we indented this li
indentedLI = listItem;
} else {
// Need to make a div to put things in if we haven't already
if (!curPositionedDiv) {
if (curNode->IsHTMLElement(nsGkAtoms::div)) {
curPositionedDiv = curNode->AsElement();
mNewBlock = curPositionedDiv;
curList = nullptr;
continue;
}
rv = SplitAsNeeded(*nsGkAtoms::div, curParent, offset);
NS_ENSURE_SUCCESS(rv, rv);
curPositionedDiv = htmlEditor->CreateNode(nsGkAtoms::div, curParent,
offset);
NS_ENSURE_STATE(curPositionedDiv);
// Remember our new block for postprocessing
mNewBlock = curPositionedDiv;
// curPositionedDiv is now the correct thing to put curNode in
}
// Tuck the node into the end of the active blockquote
rv = htmlEditor->MoveNode(curNode, curPositionedDiv, -1);
NS_ENSURE_SUCCESS(rv, rv);
// Forget curList, if any
curList = nullptr;
}
}
}
return NS_OK;
}
nsresult
HTMLEditRules::DidAbsolutePosition()
{
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor;
nsCOMPtr<nsIDOMElement> elt =
static_cast<nsIDOMElement*>(GetAsDOMNode(mNewBlock));
return absPosHTMLEditor->AbsolutelyPositionElement(elt, true);
}
nsresult
HTMLEditRules::WillRemoveAbsolutePosition(Selection* aSelection,
bool* aCancel,
bool* aHandled) {
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
WillInsert(*aSelection, aCancel);
// initialize out param
// we want to ignore aCancel from WillInsert()
*aCancel = false;
*aHandled = true;
nsCOMPtr<nsIDOMElement> elt;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt));
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor;
return absPosHTMLEditor->AbsolutelyPositionElement(elt, false);
}
nsresult
HTMLEditRules::WillRelativeChangeZIndex(Selection* aSelection,
int32_t aChange,
bool* aCancel,
bool* aHandled)
{
if (!aSelection || !aCancel || !aHandled) {
return NS_ERROR_NULL_POINTER;
}
WillInsert(*aSelection, aCancel);
// initialize out param
// we want to ignore aCancel from WillInsert()
*aCancel = false;
*aHandled = true;
nsCOMPtr<nsIDOMElement> elt;
NS_ENSURE_STATE(mHTMLEditor);
nsresult rv =
mHTMLEditor->GetAbsolutelyPositionedSelectionContainer(getter_AddRefs(elt));
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(mHTMLEditor);
AutoSelectionRestorer selectionRestorer(aSelection, mHTMLEditor);
NS_ENSURE_STATE(mHTMLEditor);
nsCOMPtr<nsIHTMLAbsPosEditor> absPosHTMLEditor = mHTMLEditor;
int32_t zIndex;
return absPosHTMLEditor->RelativeChangeElementZIndex(elt, aChange, &zIndex);
}
NS_IMETHODIMP
HTMLEditRules::DocumentModified()
{
nsContentUtils::AddScriptRunner(
NewRunnableMethod(this, &HTMLEditRules::DocumentModifiedWorker));
return NS_OK;
}
void
HTMLEditRules::DocumentModifiedWorker()
{
if (!mHTMLEditor) {
return;
}
// DeleteNode below may cause a flush, which could destroy the editor
nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker;
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
RefPtr<Selection> selection = htmlEditor->GetSelection();
if (!selection) {
return;
}
// Delete our bogus node, if we have one, since the document might not be
// empty any more.
if (mBogusNode) {
htmlEditor->DeleteNode(mBogusNode);
mBogusNode = nullptr;
}
// Try to recreate the bogus node if needed.
CreateBogusNodeIfNeeded(selection);
}
} // namespace mozilla