gecko-dev/editor/libeditor/HTMLEditRules.cpp

8892 строки
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
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) {
// 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