mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-19 21:44:04 +00:00
c013bde6aa
At first, HTMLEditor::GetActiveEditingHost might return null in this situation, we should check whether nullptr is returned. At second, SplitNodeDeep returns error since curent is design mode and selection node has no parent. So we should check error. MozReview-Commit-ID: 2YlWXPNtf80 --HG-- extra : rebase_source : 5e9752353cdf8db906e94391e9660f61336a9614
8888 lines
309 KiB
C++
8888 lines
309 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)
|
|
{
|
|
if (NS_WARN_IF(!aTextEditor)) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
InitFields();
|
|
|
|
mHTMLEditor = aTextEditor->AsHTMLEditor();
|
|
if (NS_WARN_IF(!mHTMLEditor)) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
// 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(*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);
|
|
|
|
// When there is an active editing host (the <body> if it's in designMode)
|
|
// and a block which becomes the parent of line breaker is in it, do the
|
|
// standard thing.
|
|
nsCOMPtr<Element> host = htmlEditor->GetActiveEditingHost();
|
|
if (host && !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.AsString(), 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(*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(*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(*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 nsTArray 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());
|
|
int32_t offset =
|
|
mHTMLEditor->SplitNodeDeep(*para, *selNode->AsContent(), *aOffset,
|
|
HTMLEditor::EmptyContainers::yes,
|
|
getter_AddRefs(leftPara),
|
|
getter_AddRefs(rightPara));
|
|
if (NS_WARN_IF(offset == -1)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
// 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;
|
|
}
|
|
|
|
if (NS_WARN_IF(!mNewBlock)) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
// get the (collapsed) selection location
|
|
nsCOMPtr<nsIDOMNode> selNode;
|
|
int32_t selOffset;
|
|
nsresult rv =
|
|
EditorBase::GetStartNodeAndOffset(aSelection,
|
|
getter_AddRefs(selNode), &selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// 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);
|
|
bool nodeBefore, nodeAfter;
|
|
rv = nsRange::CompareNodeToRange(mNewBlock, 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.
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<nsINode> tmp = mHTMLEditor->GetLastEditableChild(*mNewBlock);
|
|
if (!tmp) {
|
|
tmp = mNewBlock;
|
|
}
|
|
uint32_t endPoint;
|
|
if (EditorBase::IsTextNode(tmp) ||
|
|
mHTMLEditor->IsContainer(tmp)) {
|
|
endPoint = tmp->Length();
|
|
} 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.
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<nsINode> tmp = mHTMLEditor->GetFirstEditableChild(*mNewBlock);
|
|
if (!tmp) {
|
|
tmp = mNewBlock;
|
|
}
|
|
int32_t offset;
|
|
if (EditorBase::IsTextNode(tmp) ||
|
|
mHTMLEditor->IsContainer(tmp)) {
|
|
tmp = EditorBase::GetNodeLocation(tmp, &offset);
|
|
}
|
|
return aSelection->Collapse(tmp, 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
HTMLEditRules::CheckInterlinePosition(Selection& aSelection)
|
|
{
|
|
// If the selection isn't collapsed, do nothing.
|
|
if (!aSelection.Collapsed()) {
|
|
return;
|
|
}
|
|
|
|
NS_ENSURE_TRUE_VOID(mHTMLEditor);
|
|
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
|
|
|
|
// Get the (collapsed) selection location
|
|
NS_ENSURE_TRUE_VOID(aSelection.GetRangeAt(0) &&
|
|
aSelection.GetRangeAt(0)->GetStartParent());
|
|
OwningNonNull<nsINode> selNode = *aSelection.GetRangeAt(0)->GetStartParent();
|
|
int32_t selOffset = aSelection.GetRangeAt(0)->StartOffset();
|
|
|
|
// First, let's check to see if we are after a <br>. We take care of this
|
|
// special-case first so that we don't accidentally fall through into one of
|
|
// the other conditionals.
|
|
nsCOMPtr<nsIContent> node =
|
|
htmlEditor->GetPriorHTMLNode(selNode, selOffset, true);
|
|
if (node && node->IsHTMLElement(nsGkAtoms::br)) {
|
|
aSelection.SetInterlinePosition(true);
|
|
return;
|
|
}
|
|
|
|
// Are we after a block? If so try set caret to following content
|
|
node = htmlEditor->GetPriorHTMLSibling(selNode, selOffset);
|
|
if (node && IsBlockNode(*node)) {
|
|
aSelection.SetInterlinePosition(true);
|
|
return;
|
|
}
|
|
|
|
// Are we before a block? If so try set caret to prior content
|
|
node = htmlEditor->GetNextHTMLSibling(selNode, selOffset);
|
|
if (node && IsBlockNode(*node)) {
|
|
aSelection.SetInterlinePosition(false);
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::AdjustSelection(Selection* aSelection,
|
|
nsIEditor::EDirection aAction)
|
|
{
|
|
NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
|
|
|
|
// if the selection isn't collapsed, do nothing.
|
|
// moose: one thing to do instead is check for the case of
|
|
// only a single break selected, and collapse it. Good thing? Beats me.
|
|
if (!aSelection->Collapsed()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// get the (collapsed) selection location
|
|
nsCOMPtr<nsINode> selNode, temp;
|
|
int32_t selOffset;
|
|
nsresult rv =
|
|
EditorBase::GetStartNodeAndOffset(aSelection,
|
|
getter_AddRefs(selNode), &selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
temp = selNode;
|
|
|
|
// are we in an editable node?
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
while (!mHTMLEditor->IsEditable(selNode)) {
|
|
// scan up the tree until we find an editable place to be
|
|
selNode = EditorBase::GetNodeLocation(temp, &selOffset);
|
|
NS_ENSURE_TRUE(selNode, NS_ERROR_FAILURE);
|
|
temp = selNode;
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
}
|
|
|
|
// make sure we aren't in an empty block - user will see no cursor. If this
|
|
// is happening, put a <br> in the block if allowed.
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<Element> theblock = mHTMLEditor->GetBlock(*selNode);
|
|
|
|
if (theblock && mHTMLEditor->IsEditable(theblock)) {
|
|
bool bIsEmptyNode;
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
rv = mHTMLEditor->IsEmptyNode(theblock, &bIsEmptyNode, false, false);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
// check if br can go into the destination node
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
if (bIsEmptyNode && mHTMLEditor->CanContainTag(*selNode, *nsGkAtoms::br)) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<Element> rootNode = mHTMLEditor->GetRoot();
|
|
NS_ENSURE_TRUE(rootNode, NS_ERROR_FAILURE);
|
|
if (selNode == rootNode) {
|
|
// Our root node is completely empty. Don't add a <br> here.
|
|
// AfterEditInner() will add one for us when it calls
|
|
// CreateBogusNodeIfNeeded()!
|
|
return NS_OK;
|
|
}
|
|
|
|
// we know we can skip the rest of this routine given the cirumstance
|
|
return CreateMozBR(GetAsDOMNode(selNode), selOffset);
|
|
}
|
|
}
|
|
|
|
// are we in a text node?
|
|
if (EditorBase::IsTextNode(selNode)) {
|
|
return NS_OK; // we LIKE it when we are in a text node. that RULZ
|
|
}
|
|
|
|
// do we need to insert a special mozBR? We do if we are:
|
|
// 1) prior node is in same block where selection is AND
|
|
// 2) prior node is a br AND
|
|
// 3) that br is not visible
|
|
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<nsIContent> nearNode =
|
|
mHTMLEditor->GetPriorHTMLNode(selNode, selOffset);
|
|
if (nearNode) {
|
|
// is nearNode also a descendant of same block?
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<Element> block = mHTMLEditor->GetBlock(*selNode);
|
|
nsCOMPtr<Element> nearBlock = mHTMLEditor->GetBlockNodeParent(nearNode);
|
|
if (block && block == nearBlock) {
|
|
if (nearNode && TextEditUtils::IsBreak(nearNode)) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
if (!mHTMLEditor->IsVisBreak(nearNode)) {
|
|
// need to insert special moz BR. Why? Because if we don't
|
|
// the user will see no new line for the break. Also, things
|
|
// like table cells won't grow in height.
|
|
nsCOMPtr<nsIDOMNode> brNode;
|
|
rv = CreateMozBR(GetAsDOMNode(selNode), selOffset,
|
|
getter_AddRefs(brNode));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
nsCOMPtr<nsIDOMNode> brParent =
|
|
EditorBase::GetNodeLocation(brNode, &selOffset);
|
|
// selection stays *before* moz-br, sticking to it
|
|
aSelection->SetInterlinePosition(true);
|
|
rv = aSelection->Collapse(brParent, selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
} else {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<nsIContent> nextNode =
|
|
mHTMLEditor->GetNextHTMLNode(nearNode, true);
|
|
if (nextNode && TextEditUtils::IsMozBR(nextNode)) {
|
|
// selection between br and mozbr. make it stick to mozbr
|
|
// so that it will be on blank line.
|
|
aSelection->SetInterlinePosition(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// we aren't in a textnode: are we adjacent to text or a break or an image?
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nearNode = mHTMLEditor->GetPriorHTMLNode(selNode, selOffset, true);
|
|
if (nearNode && (TextEditUtils::IsBreak(nearNode) ||
|
|
EditorBase::IsTextNode(nearNode) ||
|
|
HTMLEditUtils::IsImage(nearNode) ||
|
|
nearNode->IsHTMLElement(nsGkAtoms::hr))) {
|
|
// this is a good place for the caret to be
|
|
return NS_OK;
|
|
}
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nearNode = mHTMLEditor->GetNextHTMLNode(selNode, selOffset, true);
|
|
if (nearNode && (TextEditUtils::IsBreak(nearNode) ||
|
|
EditorBase::IsTextNode(nearNode) ||
|
|
nearNode->IsAnyOfHTMLElements(nsGkAtoms::img,
|
|
nsGkAtoms::hr))) {
|
|
return NS_OK; // this is a good place for the caret to be
|
|
}
|
|
|
|
// look for a nearby text node.
|
|
// prefer the correct direction.
|
|
nsCOMPtr<nsIDOMNode> nearNodeDOM = GetAsDOMNode(nearNode);
|
|
rv = FindNearSelectableNode(GetAsDOMNode(selNode), selOffset, aAction,
|
|
address_of(nearNodeDOM));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
nearNode = do_QueryInterface(nearNodeDOM);
|
|
|
|
if (!nearNode) {
|
|
return NS_OK;
|
|
}
|
|
EditorDOMPoint pt = GetGoodSelPointForNode(*nearNode, aAction);
|
|
rv = aSelection->Collapse(pt.node, pt.offset);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
|
|
nsresult
|
|
HTMLEditRules::FindNearSelectableNode(nsIDOMNode* aSelNode,
|
|
int32_t aSelOffset,
|
|
nsIEditor::EDirection& aDirection,
|
|
nsCOMPtr<nsIDOMNode>* outSelectableNode)
|
|
{
|
|
NS_ENSURE_TRUE(aSelNode && outSelectableNode, NS_ERROR_NULL_POINTER);
|
|
*outSelectableNode = nullptr;
|
|
|
|
nsCOMPtr<nsIDOMNode> nearNode, curNode;
|
|
if (aDirection == nsIEditor::ePrevious) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv =
|
|
mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset, address_of(nearNode));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
} else {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv =
|
|
mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset, address_of(nearNode));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
// Try the other direction then.
|
|
if (!nearNode) {
|
|
if (aDirection == nsIEditor::ePrevious) {
|
|
aDirection = nsIEditor::eNext;
|
|
} else {
|
|
aDirection = nsIEditor::ePrevious;
|
|
}
|
|
|
|
if (aDirection == nsIEditor::ePrevious) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->GetPriorHTMLNode(aSelNode, aSelOffset,
|
|
address_of(nearNode));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
} else {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->GetNextHTMLNode(aSelNode, aSelOffset,
|
|
address_of(nearNode));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
}
|
|
|
|
// scan in the right direction until we find an eligible text node,
|
|
// but don't cross any breaks, images, or table elements.
|
|
while (nearNode && !(EditorBase::IsTextNode(nearNode) ||
|
|
TextEditUtils::IsBreak(nearNode) ||
|
|
HTMLEditUtils::IsImage(nearNode))) {
|
|
curNode = nearNode;
|
|
if (aDirection == nsIEditor::ePrevious) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv =
|
|
mHTMLEditor->GetPriorHTMLNode(curNode, address_of(nearNode));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
} else {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->GetNextHTMLNode(curNode, address_of(nearNode));
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
}
|
|
|
|
if (nearNode) {
|
|
// don't cross any table elements
|
|
if (InDifferentTableElements(nearNode, aSelNode)) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// otherwise, ok, we have found a good spot to put the selection
|
|
*outSelectableNode = do_QueryInterface(nearNode);
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
bool
|
|
HTMLEditRules::InDifferentTableElements(nsIDOMNode* aNode1,
|
|
nsIDOMNode* aNode2)
|
|
{
|
|
nsCOMPtr<nsINode> node1 = do_QueryInterface(aNode1);
|
|
nsCOMPtr<nsINode> node2 = do_QueryInterface(aNode2);
|
|
return InDifferentTableElements(node1, node2);
|
|
}
|
|
|
|
bool
|
|
HTMLEditRules::InDifferentTableElements(nsINode* aNode1,
|
|
nsINode* aNode2)
|
|
{
|
|
MOZ_ASSERT(aNode1 && aNode2);
|
|
|
|
while (aNode1 && !HTMLEditUtils::IsTableElement(aNode1)) {
|
|
aNode1 = aNode1->GetParentNode();
|
|
}
|
|
|
|
while (aNode2 && !HTMLEditUtils::IsTableElement(aNode2)) {
|
|
aNode2 = aNode2->GetParentNode();
|
|
}
|
|
|
|
return aNode1 != aNode2;
|
|
}
|
|
|
|
|
|
nsresult
|
|
HTMLEditRules::RemoveEmptyNodes()
|
|
{
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
|
|
|
|
// Some general notes on the algorithm used here: the goal is to examine all
|
|
// the nodes in mDocChangeRange, and remove the empty ones. We do this by
|
|
// using a content iterator to traverse all the nodes in the range, and
|
|
// placing the empty nodes into an array. After finishing the iteration, we
|
|
// delete the empty nodes in the array. (They cannot be deleted as we find
|
|
// them because that would invalidate the iterator.)
|
|
//
|
|
// Since checking to see if a node is empty can be costly for nodes with many
|
|
// descendants, there are some optimizations made. I rely on the fact that
|
|
// the iterator is post-order: it will visit children of a node before
|
|
// visiting the parent node. So if I find that a child node is not empty, I
|
|
// know that its parent is not empty without even checking. So I put the
|
|
// parent on a "skipList" which is just a voidArray of nodes I can skip the
|
|
// empty check on. If I encounter a node on the skiplist, i skip the
|
|
// processing for that node and replace its slot in the skiplist with that
|
|
// node's parent.
|
|
//
|
|
// An interesting idea is to go ahead and regard parent nodes that are NOT on
|
|
// the skiplist as being empty (without even doing the IsEmptyNode check) on
|
|
// the theory that if they weren't empty, we would have encountered a
|
|
// non-empty child earlier and thus put this parent node on the skiplist.
|
|
//
|
|
// Unfortunately I can't use that strategy here, because the range may
|
|
// include some children of a node while excluding others. Thus I could find
|
|
// all the _examined_ children empty, but still not have an empty parent.
|
|
|
|
// need an iterator
|
|
nsCOMPtr<nsIContentIterator> iter = NS_NewContentIterator();
|
|
|
|
nsresult rv = iter->Init(mDocChangeRange);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsTArray<OwningNonNull<nsINode>> arrayOfEmptyNodes, arrayOfEmptyCites, skipList;
|
|
|
|
// Check for empty nodes
|
|
while (!iter->IsDone()) {
|
|
OwningNonNull<nsINode> node = *iter->GetCurrentNode();
|
|
|
|
nsCOMPtr<nsINode> parent = node->GetParentNode();
|
|
|
|
size_t idx = skipList.IndexOf(node);
|
|
if (idx != skipList.NoIndex) {
|
|
// This node is on our skip list. Skip processing for this node, and
|
|
// replace its value in the skip list with the value of its parent
|
|
if (parent) {
|
|
skipList[idx] = parent;
|
|
}
|
|
} else {
|
|
bool bIsCandidate = false;
|
|
bool bIsEmptyNode = false;
|
|
bool bIsMailCite = false;
|
|
|
|
if (node->IsElement()) {
|
|
if (node->IsHTMLElement(nsGkAtoms::body)) {
|
|
// Don't delete the body
|
|
} else if ((bIsMailCite = HTMLEditUtils::IsMailCite(node)) ||
|
|
node->IsHTMLElement(nsGkAtoms::a) ||
|
|
HTMLEditUtils::IsInlineStyle(node) ||
|
|
HTMLEditUtils::IsList(node) ||
|
|
node->IsHTMLElement(nsGkAtoms::div)) {
|
|
// Only consider certain nodes to be empty for purposes of removal
|
|
bIsCandidate = true;
|
|
} else if (HTMLEditUtils::IsFormatNode(node) ||
|
|
HTMLEditUtils::IsListItem(node) ||
|
|
node->IsHTMLElement(nsGkAtoms::blockquote)) {
|
|
// These node types are candidates if selection is not in them. If
|
|
// it is one of these, don't delete if selection inside. This is so
|
|
// we can create empty headings, etc., for the user to type into.
|
|
bool bIsSelInNode;
|
|
rv = SelectionEndpointInNode(node, &bIsSelInNode);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!bIsSelInNode) {
|
|
bIsCandidate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bIsCandidate) {
|
|
// We delete mailcites even if they have a solo br in them. Other
|
|
// nodes we require to be empty.
|
|
rv = htmlEditor->IsEmptyNode(node->AsDOMNode(), &bIsEmptyNode,
|
|
bIsMailCite, true);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (bIsEmptyNode) {
|
|
if (bIsMailCite) {
|
|
// mailcites go on a separate list from other empty nodes
|
|
arrayOfEmptyCites.AppendElement(*node);
|
|
} else {
|
|
arrayOfEmptyNodes.AppendElement(*node);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bIsEmptyNode && parent) {
|
|
// put parent on skip list
|
|
skipList.AppendElement(*parent);
|
|
}
|
|
}
|
|
|
|
iter->Next();
|
|
}
|
|
|
|
// now delete the empty nodes
|
|
for (auto& delNode : arrayOfEmptyNodes) {
|
|
if (htmlEditor->IsModifiableNode(delNode)) {
|
|
rv = htmlEditor->DeleteNode(delNode);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
}
|
|
|
|
// Now delete the empty mailcites. This is a separate step because we want
|
|
// to pull out any br's and preserve them.
|
|
for (auto& delNode : arrayOfEmptyCites) {
|
|
bool bIsEmptyNode;
|
|
rv = htmlEditor->IsEmptyNode(delNode, &bIsEmptyNode, false, true);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!bIsEmptyNode) {
|
|
// We are deleting a cite that has just a br. We want to delete cite,
|
|
// but preserve br.
|
|
nsCOMPtr<nsINode> parent = delNode->GetParentNode();
|
|
int32_t offset = parent ? parent->IndexOf(delNode) : -1;
|
|
nsCOMPtr<Element> br = htmlEditor->CreateBR(parent, offset);
|
|
NS_ENSURE_STATE(br);
|
|
}
|
|
rv = htmlEditor->DeleteNode(delNode);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::SelectionEndpointInNode(nsINode* aNode,
|
|
bool* aResult)
|
|
{
|
|
NS_ENSURE_TRUE(aNode && aResult, NS_ERROR_NULL_POINTER);
|
|
|
|
nsIDOMNode* node = aNode->AsDOMNode();
|
|
|
|
*aResult = false;
|
|
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
|
|
NS_ENSURE_STATE(selection);
|
|
|
|
uint32_t rangeCount = selection->RangeCount();
|
|
for (uint32_t rangeIdx = 0; rangeIdx < rangeCount; ++rangeIdx) {
|
|
RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
|
|
nsCOMPtr<nsIDOMNode> startParent, endParent;
|
|
range->GetStartContainer(getter_AddRefs(startParent));
|
|
if (startParent) {
|
|
if (node == startParent) {
|
|
*aResult = true;
|
|
return NS_OK;
|
|
}
|
|
if (EditorUtils::IsDescendantOf(startParent, node)) {
|
|
*aResult = true;
|
|
return NS_OK;
|
|
}
|
|
}
|
|
range->GetEndContainer(getter_AddRefs(endParent));
|
|
if (startParent == endParent) {
|
|
continue;
|
|
}
|
|
if (endParent) {
|
|
if (node == endParent) {
|
|
*aResult = true;
|
|
return NS_OK;
|
|
}
|
|
if (EditorUtils::IsDescendantOf(endParent, node)) {
|
|
*aResult = true;
|
|
return NS_OK;
|
|
}
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
/**
|
|
* IsEmptyInline: Return true if aNode is an empty inline container
|
|
*/
|
|
bool
|
|
HTMLEditRules::IsEmptyInline(nsINode& aNode)
|
|
{
|
|
NS_ENSURE_TRUE(mHTMLEditor, false);
|
|
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
|
|
|
|
if (IsInlineNode(aNode) && htmlEditor->IsContainer(&aNode)) {
|
|
bool isEmpty = true;
|
|
htmlEditor->IsEmptyNode(&aNode, &isEmpty);
|
|
return isEmpty;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool
|
|
HTMLEditRules::ListIsEmptyLine(nsTArray<OwningNonNull<nsINode>>& aArrayOfNodes)
|
|
{
|
|
// We have a list of nodes which we are candidates for being moved into a new
|
|
// block. Determine if it's anything more than a blank line. Look for
|
|
// editable content above and beyond one single BR.
|
|
NS_ENSURE_TRUE(aArrayOfNodes.Length(), true);
|
|
|
|
NS_ENSURE_TRUE(mHTMLEditor, false);
|
|
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
|
|
|
|
int32_t brCount = 0;
|
|
|
|
for (auto& node : aArrayOfNodes) {
|
|
if (!htmlEditor->IsEditable(node)) {
|
|
continue;
|
|
}
|
|
if (TextEditUtils::IsBreak(node)) {
|
|
// First break doesn't count
|
|
if (brCount) {
|
|
return false;
|
|
}
|
|
brCount++;
|
|
} else if (IsEmptyInline(node)) {
|
|
// Empty inline, keep looking
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
nsresult
|
|
HTMLEditRules::PopListItem(nsIContent& aListItem,
|
|
bool* aOutOfList)
|
|
{
|
|
// init out params
|
|
if (aOutOfList) {
|
|
*aOutOfList = false;
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> kungFuDeathGrip(&aListItem);
|
|
Unused << kungFuDeathGrip;
|
|
|
|
nsCOMPtr<nsINode> curParent = aListItem.GetParentNode();
|
|
if (NS_WARN_IF(!curParent)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
int32_t offset = curParent->IndexOf(&aListItem);
|
|
|
|
if (!HTMLEditUtils::IsListItem(&aListItem)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// if it's first or last list item, don't need to split the list
|
|
// otherwise we do.
|
|
nsCOMPtr<nsINode> curParPar = curParent->GetParentNode();
|
|
int32_t parOffset = curParPar ? curParPar->IndexOf(curParent) : -1;
|
|
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
bool bIsFirstListItem = mHTMLEditor->IsFirstEditableChild(&aListItem);
|
|
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
bool bIsLastListItem = mHTMLEditor->IsLastEditableChild(&aListItem);
|
|
|
|
if (!bIsFirstListItem && !bIsLastListItem) {
|
|
// split the list
|
|
ErrorResult rv;
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
mHTMLEditor->SplitNode(*curParent->AsContent(), offset, rv);
|
|
if (NS_WARN_IF(rv.Failed())) {
|
|
return rv.StealNSResult();
|
|
}
|
|
}
|
|
|
|
if (!bIsFirstListItem) {
|
|
parOffset++;
|
|
}
|
|
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->MoveNode(&aListItem, curParPar, parOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
// unwrap list item contents if they are no longer in a list
|
|
if (!HTMLEditUtils::IsList(curParPar) &&
|
|
HTMLEditUtils::IsListItem(&aListItem)) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
rv = mHTMLEditor->RemoveBlockContainer(*aListItem.AsElement());
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (aOutOfList) {
|
|
*aOutOfList = true;
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::RemoveListStructure(Element& aList)
|
|
{
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
|
|
while (aList.GetFirstChild()) {
|
|
OwningNonNull<nsIContent> child = *aList.GetFirstChild();
|
|
|
|
if (HTMLEditUtils::IsListItem(child)) {
|
|
bool isOutOfList;
|
|
// Keep popping it out until it's not in a list anymore
|
|
do {
|
|
nsresult rv = PopListItem(child, &isOutOfList);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
} while (!isOutOfList);
|
|
} else if (HTMLEditUtils::IsList(child)) {
|
|
nsresult rv = RemoveListStructure(*child->AsElement());
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
} else {
|
|
// Delete any non-list items for now
|
|
nsresult rv = htmlEditor->DeleteNode(child);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
}
|
|
|
|
// Delete the now-empty list
|
|
nsresult rv = htmlEditor->RemoveBlockContainer(aList);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::ConfirmSelectionInBody()
|
|
{
|
|
// get the body
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(mHTMLEditor->GetRoot());
|
|
NS_ENSURE_TRUE(rootElement, NS_ERROR_UNEXPECTED);
|
|
|
|
// get the selection
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
RefPtr<Selection> selection = mHTMLEditor->GetSelection();
|
|
NS_ENSURE_STATE(selection);
|
|
|
|
// get the selection start location
|
|
nsCOMPtr<nsIDOMNode> selNode, temp, parent;
|
|
int32_t selOffset;
|
|
nsresult rv =
|
|
EditorBase::GetStartNodeAndOffset(selection,
|
|
getter_AddRefs(selNode), &selOffset);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
temp = selNode;
|
|
|
|
// check that selNode is inside body
|
|
while (temp && !TextEditUtils::IsBody(temp)) {
|
|
temp->GetParentNode(getter_AddRefs(parent));
|
|
temp = parent;
|
|
}
|
|
|
|
// if we aren't in the body, force the issue
|
|
if (!temp) {
|
|
// uncomment this to see when we get bad selections
|
|
// NS_NOTREACHED("selection not in body");
|
|
selection->Collapse(rootElement, 0);
|
|
}
|
|
|
|
// get the selection end location
|
|
rv = EditorBase::GetEndNodeAndOffset(selection,
|
|
getter_AddRefs(selNode), &selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
temp = selNode;
|
|
|
|
// check that selNode is inside body
|
|
while (temp && !TextEditUtils::IsBody(temp)) {
|
|
rv = temp->GetParentNode(getter_AddRefs(parent));
|
|
temp = parent;
|
|
}
|
|
|
|
// if we aren't in the body, force the issue
|
|
if (!temp) {
|
|
// uncomment this to see when we get bad selections
|
|
// NS_NOTREACHED("selection not in body");
|
|
selection->Collapse(rootElement, 0);
|
|
}
|
|
|
|
// XXX This is the result of the last call of GetParentNode(), it doesn't
|
|
// make sense...
|
|
return rv;
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::UpdateDocChangeRange(nsRange* aRange)
|
|
{
|
|
// first make sure aRange is in the document. It might not be if
|
|
// portions of our editting action involved manipulating nodes
|
|
// prior to placing them in the document (e.g., populating a list item
|
|
// before placing it in its list)
|
|
nsCOMPtr<nsIDOMNode> startNode;
|
|
nsresult rv = aRange->GetStartContainer(getter_AddRefs(startNode));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
if (!mHTMLEditor->IsDescendantOfRoot(startNode)) {
|
|
// just return - we don't need to adjust mDocChangeRange in this case
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!mDocChangeRange) {
|
|
// clone aRange.
|
|
mDocChangeRange = aRange->CloneRange();
|
|
} else {
|
|
int16_t result;
|
|
|
|
// compare starts of ranges
|
|
rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::START_TO_START,
|
|
aRange, &result);
|
|
if (rv == NS_ERROR_NOT_INITIALIZED) {
|
|
// This will happen is mDocChangeRange is non-null, but the range is
|
|
// uninitialized. In this case we'll set the start to aRange start.
|
|
// The same test won't be needed further down since after we've set
|
|
// the start the range will be collapsed to that point.
|
|
result = 1;
|
|
rv = NS_OK;
|
|
}
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
// Positive result means mDocChangeRange start is after aRange start.
|
|
if (result > 0) {
|
|
int32_t startOffset;
|
|
rv = aRange->GetStartOffset(&startOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mDocChangeRange->SetStart(startNode, startOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
// compare ends of ranges
|
|
rv = mDocChangeRange->CompareBoundaryPoints(nsIDOMRange::END_TO_END,
|
|
aRange, &result);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
// Negative result means mDocChangeRange end is before aRange end.
|
|
if (result < 0) {
|
|
nsCOMPtr<nsIDOMNode> endNode;
|
|
int32_t endOffset;
|
|
rv = aRange->GetEndContainer(getter_AddRefs(endNode));
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = aRange->GetEndOffset(&endOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mDocChangeRange->SetEnd(endNode, endOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::InsertMozBRIfNeeded(nsINode& aNode)
|
|
{
|
|
if (!IsBlockNode(aNode)) {
|
|
return NS_OK;
|
|
}
|
|
|
|
bool isEmpty;
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->IsEmptyNode(&aNode, &isEmpty);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
if (!isEmpty) {
|
|
return NS_OK;
|
|
}
|
|
|
|
return CreateMozBR(aNode.AsDOMNode(), 0);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillCreateNode(const nsAString& aTag,
|
|
nsIDOMNode* aParent,
|
|
int32_t aPosition)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidCreateNode(const nsAString& aTag,
|
|
nsIDOMNode* aNode,
|
|
nsIDOMNode* aParent,
|
|
int32_t aPosition,
|
|
nsresult aResult)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
// assumption that Join keeps the righthand node
|
|
nsresult rv = mUtilRange->SelectNode(aNode);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillInsertNode(nsIDOMNode* aNode,
|
|
nsIDOMNode* aParent,
|
|
int32_t aPosition)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidInsertNode(nsIDOMNode* aNode,
|
|
nsIDOMNode* aParent,
|
|
int32_t aPosition,
|
|
nsresult aResult)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
nsresult rv = mUtilRange->SelectNode(aNode);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillDeleteNode(nsIDOMNode* aChild)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
nsresult rv = mUtilRange->SelectNode(aChild);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidDeleteNode(nsIDOMNode* aChild,
|
|
nsresult aResult)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillSplitNode(nsIDOMNode* aExistingRightNode,
|
|
int32_t aOffset)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidSplitNode(nsIDOMNode* aExistingRightNode,
|
|
int32_t aOffset,
|
|
nsIDOMNode* aNewLeftNode,
|
|
nsresult aResult)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
nsresult rv = mUtilRange->SetStart(aNewLeftNode, 0);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mUtilRange->SetEnd(aExistingRightNode, 0);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillJoinNodes(nsIDOMNode* aLeftNode,
|
|
nsIDOMNode* aRightNode,
|
|
nsIDOMNode* aParent)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
// remember split point
|
|
return EditorBase::GetLengthOfDOMNode(aLeftNode, mJoinOffset);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidJoinNodes(nsIDOMNode* aLeftNode,
|
|
nsIDOMNode* aRightNode,
|
|
nsIDOMNode* aParent,
|
|
nsresult aResult)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
// assumption that Join keeps the righthand node
|
|
nsresult rv = mUtilRange->SetStart(aRightNode, mJoinOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mUtilRange->SetEnd(aRightNode, mJoinOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillInsertText(nsIDOMCharacterData* aTextNode,
|
|
int32_t aOffset,
|
|
const nsAString& aString)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidInsertText(nsIDOMCharacterData* aTextNode,
|
|
int32_t aOffset,
|
|
const nsAString& aString,
|
|
nsresult aResult)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
int32_t length = aString.Length();
|
|
nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode);
|
|
nsresult rv = mUtilRange->SetStart(theNode, aOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mUtilRange->SetEnd(theNode, aOffset+length);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillDeleteText(nsIDOMCharacterData* aTextNode,
|
|
int32_t aOffset,
|
|
int32_t aLength)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidDeleteText(nsIDOMCharacterData* aTextNode,
|
|
int32_t aOffset,
|
|
int32_t aLength,
|
|
nsresult aResult)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
nsCOMPtr<nsIDOMNode> theNode = do_QueryInterface(aTextNode);
|
|
nsresult rv = mUtilRange->SetStart(theNode, aOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mUtilRange->SetEnd(theNode, aOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::WillDeleteSelection(nsISelection* aSelection)
|
|
{
|
|
if (!mListenerEnabled) {
|
|
return NS_OK;
|
|
}
|
|
if (NS_WARN_IF(!aSelection)) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
RefPtr<Selection> selection = aSelection->AsSelection();
|
|
// get the (collapsed) selection location
|
|
nsCOMPtr<nsIDOMNode> selNode;
|
|
int32_t selOffset;
|
|
|
|
nsresult rv =
|
|
EditorBase::GetStartNodeAndOffset(selection,
|
|
getter_AddRefs(selNode), &selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mUtilRange->SetStart(selNode, selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = EditorBase::GetEndNodeAndOffset(selection,
|
|
getter_AddRefs(selNode), &selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
rv = mUtilRange->SetEnd(selNode, selOffset);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
return UpdateDocChangeRange(mUtilRange);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
HTMLEditRules::DidDeleteSelection(nsISelection *aSelection)
|
|
{
|
|
return NS_OK;
|
|
}
|
|
|
|
// Let's remove all alignment hints in the children of aNode; it can
|
|
// be an ALIGN attribute (in case we just remove it) or a CENTER
|
|
// element (here we have to remove the container and keep its
|
|
// children). We break on tables and don't look at their children.
|
|
nsresult
|
|
HTMLEditRules::RemoveAlignment(nsINode& aNode,
|
|
const nsAString& aAlignType,
|
|
bool aChildrenOnly)
|
|
{
|
|
if (EditorBase::IsTextNode(&aNode) || HTMLEditUtils::IsTable(&aNode)) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsINode> child, tmp;
|
|
if (aChildrenOnly) {
|
|
child = aNode.GetFirstChild();
|
|
} else {
|
|
child = &aNode;
|
|
}
|
|
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
|
|
tmp = child->GetNextSibling();
|
|
} else {
|
|
tmp = nullptr;
|
|
}
|
|
|
|
if (child->IsHTMLElement(nsGkAtoms::center)) {
|
|
// the current node is a CENTER element
|
|
// first remove children's alignment
|
|
nsresult rv = RemoveAlignment(*child, aAlignType, true);
|
|
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);
|
|
rv = mHTMLEditor->RemoveContainer(child->AsElement());
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
} else if (IsBlockNode(*child) || child->IsHTMLElement(nsGkAtoms::hr)) {
|
|
// the current node is a block element
|
|
if (HTMLEditUtils::SupportsAlignAttr(*child)) {
|
|
// remove the ALIGN attribute if this element can have it
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->RemoveAttribute(child->AsElement(),
|
|
nsGkAtoms::align);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
if (useCSS) {
|
|
if (child->IsAnyOfHTMLElements(nsGkAtoms::table, nsGkAtoms::hr)) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv =
|
|
mHTMLEditor->SetAttributeOrEquivalent(child->AsElement(),
|
|
nsGkAtoms::align,
|
|
aAlignType, false);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
} else {
|
|
nsAutoString dummyCssValue;
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
nsresult rv = mHTMLEditor->mCSSEditUtils->RemoveCSSInlineStyle(
|
|
*child,
|
|
nsGkAtoms::textAlign,
|
|
dummyCssValue);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
}
|
|
}
|
|
if (!child->IsHTMLElement(nsGkAtoms::table)) {
|
|
// unless this is a table, look at children
|
|
nsresult rv = RemoveAlignment(*child, aAlignType, true);
|
|
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(nsINode& aNode,
|
|
bool aStarts)
|
|
{
|
|
nsCOMPtr<nsINode> child;
|
|
if (aStarts) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
child = mHTMLEditor->GetFirstEditableChild(aNode);
|
|
} else {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
child = mHTMLEditor->GetLastEditableChild(aNode);
|
|
}
|
|
NS_ENSURE_TRUE(child, NS_OK);
|
|
bool foundCR = false;
|
|
if (IsBlockNode(*child) || child->IsHTMLElement(nsGkAtoms::br)) {
|
|
foundCR = true;
|
|
} else {
|
|
nsCOMPtr<nsINode> sibling;
|
|
if (aStarts) {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
sibling = mHTMLEditor->GetPriorHTMLSibling(&aNode);
|
|
} else {
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
sibling = mHTMLEditor->GetNextHTMLSibling(&aNode);
|
|
}
|
|
if (sibling) {
|
|
if (IsBlockNode(*sibling) || sibling->IsHTMLElement(nsGkAtoms::br)) {
|
|
foundCR = true;
|
|
}
|
|
} else {
|
|
foundCR = true;
|
|
}
|
|
}
|
|
if (!foundCR) {
|
|
int32_t offset = 0;
|
|
if (!aStarts) {
|
|
offset = aNode.GetChildCount();
|
|
}
|
|
NS_ENSURE_STATE(mHTMLEditor);
|
|
RefPtr<Element> brNode = mHTMLEditor->CreateBR(&aNode, offset);
|
|
if (NS_WARN_IF(!brNode)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
HTMLEditRules::MakeSureElemStartsOrEndsOnCR(nsINode& 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, 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)) {
|
|
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
|