gecko-dev/editor/libeditor/EditorBase.cpp
Masayuki Nakano 9b40433ef6 Bug 1461708 - part 7: Make EventStateManager::HandleMiddleClickPaste() dispatch ePaste event by itself r=smaug
This is preparation of the last patch.  Even if no editor is clicked with
middle button, we need to do:
- collapse Selection at the clicked point.
- dispatch "paste" event.

Therefore, HandleMiddleClickPaste() should dispatch ePaste event by itself
and each editor methods should have a bool argument which the caller wants
ePaste event automatically.

Note that Chromium dispatches "paste" event and pastes clipboard content
into clicked editor even if preceding "auxclick" event is consumed.
However, our traditional behavior is not dispatching "paste" event nor
pasting clipboard content.  Unless Chromium developer keeps their odd
behavior, we should keep our traditional behavior since our behavior is
conforming to DOM event model.

Differential Revision: https://phabricator.services.mozilla.com/D7854

--HG--
extra : moz-landing-system : lando
2018-10-10 12:05:39 +00:00

5186 lines
159 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "mozilla/EditorBase.h"
#include "mozilla/DebugOnly.h" // for DebugOnly
#include "mozilla/Encoding.h" // for Encoding
#include <stdio.h> // for nullptr, stdout
#include <string.h> // for strcmp
#include "ChangeAttributeTransaction.h" // for ChangeAttributeTransaction
#include "CompositionTransaction.h" // for CompositionTransaction
#include "CreateElementTransaction.h" // for CreateElementTransaction
#include "DeleteNodeTransaction.h" // for DeleteNodeTransaction
#include "DeleteRangeTransaction.h" // for DeleteRangeTransaction
#include "DeleteTextTransaction.h" // for DeleteTextTransaction
#include "EditAggregateTransaction.h" // for EditAggregateTransaction
#include "EditorEventListener.h" // for EditorEventListener
#include "HTMLEditRules.h" // for HTMLEditRules
#include "InsertNodeTransaction.h" // for InsertNodeTransaction
#include "InsertTextTransaction.h" // for InsertTextTransaction
#include "JoinNodeTransaction.h" // for JoinNodeTransaction
#include "PlaceholderTransaction.h" // for PlaceholderTransaction
#include "SplitNodeTransaction.h" // for SplitNodeTransaction
#include "TextEditUtils.h" // for TextEditUtils
#include "mozilla/CheckedInt.h" // for CheckedInt
#include "mozilla/ComputedStyle.h" // for ComputedStyle
#include "mozilla/CSSEditUtils.h" // for CSSEditUtils
#include "mozilla/EditAction.h" // for EditSubAction
#include "mozilla/EditorDOMPoint.h" // for EditorDOMPoint
#include "mozilla/EditorSpellCheck.h" // for EditorSpellCheck
#include "mozilla/EditorUtils.h" // for various helper classes.
#include "mozilla/EditTransactionBase.h" // for EditTransactionBase
#include "mozilla/FlushType.h" // for FlushType::Frames
#include "mozilla/IMEContentObserver.h" // for IMEContentObserver
#include "mozilla/IMEStateManager.h" // for IMEStateManager
#include "mozilla/mozalloc.h" // for operator new, etc.
#include "mozilla/mozInlineSpellChecker.h" // for mozInlineSpellChecker
#include "mozilla/mozSpellChecker.h" // for mozSpellChecker
#include "mozilla/Preferences.h" // for Preferences
#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary
#include "mozilla/dom/Selection.h" // for Selection, etc.
#include "mozilla/Services.h" // for GetObserverService
#include "mozilla/TextComposition.h" // for TextComposition
#include "mozilla/TextInputListener.h" // for TextInputListener
#include "mozilla/TextServicesDocument.h" // for TextServicesDocument
#include "mozilla/TextEvents.h"
#include "mozilla/TransactionManager.h" // for TransactionManager
#include "mozilla/dom/CharacterData.h" // for CharacterData
#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement
#include "mozilla/dom/EventTarget.h" // for EventTarget
#include "mozilla/dom/HTMLBodyElement.h"
#include "mozilla/dom/Text.h"
#include "mozilla/dom/Event.h"
#include "nsAString.h" // for nsAString::Length, etc.
#include "nsCCUncollectableMarker.h" // for nsCCUncollectableMarker
#include "nsCaret.h" // for nsCaret
#include "nsCaseTreatment.h"
#include "nsCharTraits.h" // for NS_IS_HIGH_SURROGATE, etc.
#include "nsComponentManagerUtils.h" // for do_CreateInstance
#include "nsComputedDOMStyle.h" // for nsComputedDOMStyle
#include "nsContentUtils.h" // for nsContentUtils
#include "nsDOMString.h" // for DOMStringIsNull
#include "nsDebug.h" // for NS_ENSURE_TRUE, etc.
#include "nsError.h" // for NS_OK, etc.
#include "nsFocusManager.h" // for nsFocusManager
#include "nsFrameSelection.h" // for nsFrameSelection
#include "nsGenericHTMLElement.h" // for nsGenericHTMLElement
#include "nsGkAtoms.h" // for nsGkAtoms, nsGkAtoms::dir
#include "nsIAbsorbingTransaction.h" // for nsIAbsorbingTransaction
#include "nsAtom.h" // for nsAtom
#include "nsIContent.h" // for nsIContent
#include "nsIDocument.h" // for nsIDocument
#include "nsIDOMEventListener.h" // for nsIDOMEventListener
#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener
#include "nsIEditActionListener.h" // for nsIEditActionListener
#include "nsIEditorObserver.h" // for nsIEditorObserver
#include "nsIEditorSpellCheck.h" // for nsIEditorSpellCheck
#include "nsIFrame.h" // for nsIFrame
#include "nsIHTMLDocument.h" // for nsIHTMLDocument
#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc.
#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc.
#include "nsINode.h" // for nsINode, etc.
#include "nsIPlaintextEditor.h" // for nsIPlaintextEditor, etc.
#include "nsIPresShell.h" // for nsIPresShell
#include "nsISelectionController.h" // for nsISelectionController, etc.
#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc.
#include "nsISupportsBase.h" // for nsISupports
#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF
#include "nsITransaction.h" // for nsITransaction
#include "nsITransactionManager.h"
#include "nsIWeakReference.h" // for nsISupportsWeakReference
#include "nsIWidget.h" // for nsIWidget, IMEState, etc.
#include "nsPIDOMWindow.h" // for nsPIDOMWindow
#include "nsPresContext.h" // for nsPresContext
#include "nsRange.h" // for nsRange
#include "nsReadableUtils.h" // for EmptyString, ToNewCString
#include "nsString.h" // for nsAutoString, nsString, etc.
#include "nsStringFwd.h" // for nsString
#include "nsStyleConsts.h" // for NS_STYLE_DIRECTION_RTL, etc.
#include "nsStyleStruct.h" // for nsStyleDisplay, nsStyleText, etc.
#include "nsStyleStructFwd.h" // for nsIFrame::StyleUIReset, etc.
#include "nsTextNode.h" // for nsTextNode
#include "nsThreadUtils.h" // for nsRunnable
#include "prtime.h" // for PR_Now
class nsIOutputStream;
class nsITransferable;
namespace mozilla {
using namespace dom;
using namespace widget;
/*****************************************************************************
* mozilla::EditorBase
*****************************************************************************/
template already_AddRefed<Element>
EditorBase::CreateNodeWithTransaction(nsAtom& aTag,
const EditorDOMPoint& aPointToInsert);
template already_AddRefed<Element>
EditorBase::CreateNodeWithTransaction(nsAtom& aTag,
const EditorRawDOMPoint& aPointToInsert);
template nsresult
EditorBase::InsertNodeWithTransaction(nsIContent& aContentToInsert,
const EditorDOMPoint& aPointToInsert);
template nsresult
EditorBase::InsertNodeWithTransaction(nsIContent& aContentToInsert,
const EditorRawDOMPoint& aPointToInsert);
template already_AddRefed<nsIContent>
EditorBase::SplitNodeWithTransaction(const EditorDOMPoint& aStartOfRightNode,
ErrorResult& aError);
template already_AddRefed<nsIContent>
EditorBase::SplitNodeWithTransaction(const EditorRawDOMPoint& aStartOfRightNode,
ErrorResult& aError);
template SplitNodeResult
EditorBase::SplitNodeDeepWithTransaction(
nsIContent& aMostAncestorToSplit,
const EditorDOMPoint& aStartOfDeepestRightNode,
SplitAtEdges aSplitAtEdges);
template SplitNodeResult
EditorBase::SplitNodeDeepWithTransaction(
nsIContent& aMostAncestorToSplit,
const EditorRawDOMPoint& aStartOfDeepestRightNode,
SplitAtEdges aSplitAtEdges);
template nsresult
EditorBase::MoveNodeWithTransaction(nsIContent& aContent,
const EditorDOMPoint& aPointToInsert);
template nsresult
EditorBase::MoveNodeWithTransaction(nsIContent& aContent,
const EditorRawDOMPoint& aPointToInsert);
EditorBase::EditorBase()
: mPlaceholderName(nullptr)
, mModCount(0)
, mFlags(0)
, mUpdateCount(0)
, mPlaceholderBatch(0)
, mTopLevelEditSubAction(EditSubAction::eNone)
, mDirection(eNone)
, mDocDirtyState(-1)
, mSpellcheckCheckboxState(eTriUnset)
, mAllowsTransactionsToChangeSelection(true)
, mDidPreDestroy(false)
, mDidPostCreate(false)
, mDispatchInputEvent(true)
, mIsInEditSubAction(false)
, mHidingCaret(false)
, mSpellCheckerDictionaryUpdated(true)
, mIsHTMLEditorClass(false)
{
}
EditorBase::~EditorBase()
{
MOZ_ASSERT(!IsInitialized() || mDidPreDestroy,
"Why PreDestroy hasn't been called?");
if (mComposition) {
mComposition->OnEditorDestroyed();
mComposition = nullptr;
}
// If this editor is still hiding the caret, we need to restore it.
HideCaret(false);
mTransactionManager = nullptr;
}
NS_IMPL_CYCLE_COLLECTION_CLASS(EditorBase)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EditorBase)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionController)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mIMEContentObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mInlineSpellChecker)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextServicesDocument)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextInputListener)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransactionManager)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionListeners)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorObservers)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocStateListeners)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget)
if (tmp->mEventListener) {
tmp->mEventListener->Disconnect();
tmp->mEventListener = nullptr;
}
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlaceholderTransaction)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSavedSel);
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRangeUpdater);
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase)
nsIDocument* currentDoc =
tmp->mRootElement ? tmp->mRootElement->GetUncomposedDoc() : nullptr;
if (currentDoc &&
nsCCUncollectableMarker::InGeneration(cb, currentDoc->GetMarkedCCGeneration())) {
return NS_SUCCESS_INTERRUPTED_TRAVERSE;
}
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionController)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMEContentObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInlineSpellChecker)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextServicesDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextInputListener)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransactionManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionListeners)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorObservers)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocStateListeners)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlaceholderTransaction)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSavedSel);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRangeUpdater);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EditorBase)
NS_INTERFACE_MAP_ENTRY(nsISelectionListener)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY(nsIEditor)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditor)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorBase)
NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorBase)
nsresult
EditorBase::Init(nsIDocument& aDocument,
Element* aRoot,
nsISelectionController* aSelectionController,
uint32_t aFlags,
const nsAString& aValue)
{
MOZ_ASSERT(mTopLevelEditSubAction == EditSubAction::eNone,
"Initializing during an edit action is an error");
// First only set flags, but other stuff shouldn't be initialized now.
// Don't move this call after initializing mDocument.
// SetFlags() can check whether it's called during initialization or not by
// them. Note that SetFlags() will be called by PostCreate().
#ifdef DEBUG
nsresult rv =
#endif
SetFlags(aFlags);
NS_ASSERTION(NS_SUCCEEDED(rv), "SetFlags() failed");
mDocument = &aDocument;
// HTML editors currently don't have their own selection controller,
// so they'll pass null as aSelCon, and we'll get the selection controller
// off of the presshell.
nsCOMPtr<nsISelectionController> selectionController;
if (aSelectionController) {
mSelectionController = aSelectionController;
selectionController = aSelectionController;
} else {
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
selectionController = do_QueryInterface(presShell);
}
MOZ_ASSERT(selectionController,
"Selection controller should be available at this point");
//set up root element if we are passed one.
if (aRoot) {
mRootElement = aRoot;
}
mUpdateCount=0;
// If this is an editor for <input> or <textarea>, the text node which
// has composition string is always recreated with same content. Therefore,
// we need to nodify mComposition of text node destruction and replacing
// composing string when this receives eCompositionChange event next time.
if (mComposition &&
mComposition->GetContainerTextNode() &&
!mComposition->GetContainerTextNode()->IsInComposedDoc()) {
mComposition->OnTextNodeRemoved();
}
// Show the caret.
selectionController->SetCaretReadOnly(false);
selectionController->SetDisplaySelection(
nsISelectionController::SELECTION_ON);
// Show all the selection reflected to user.
selectionController->SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);
MOZ_ASSERT(IsInitialized());
Selection* selection = GetSelection();
if (selection) {
selection->AddSelectionListener(this);
}
// Make sure that the editor will be destroyed properly
mDidPreDestroy = false;
// Make sure that the ediotr will be created properly
mDidPostCreate = false;
return NS_OK;
}
nsresult
EditorBase::PostCreate()
{
// Synchronize some stuff for the flags. SetFlags() will initialize
// something by the flag difference. This is first time of that, so, all
// initializations must be run. For such reason, we need to invert mFlags
// value first.
mFlags = ~mFlags;
nsresult rv = SetFlags(~mFlags);
NS_ENSURE_SUCCESS(rv, rv);
// These operations only need to happen on the first PostCreate call
if (!mDidPostCreate) {
mDidPostCreate = true;
// Set up listeners
CreateEventListeners();
rv = InstallEventListeners();
NS_ENSURE_SUCCESS(rv, rv);
// nuke the modification count, so the doc appears unmodified
// do this before we notify listeners
ResetModificationCount();
// update the UI with our state
NotifyDocumentListeners(eDocumentCreated);
NotifyDocumentListeners(eDocumentStateChanged);
}
// update nsTextStateManager and caret if we have focus
nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
if (focusedContent) {
InitializeSelection(focusedContent);
// If the text control gets reframed during focus, Focus() would not be
// called, so take a chance here to see if we need to spell check the text
// control.
mEventListener->SpellCheckIfNeeded();
IMEState newState;
rv = GetPreferredIMEState(&newState);
NS_ENSURE_SUCCESS(rv, NS_OK);
// May be null in design mode
nsCOMPtr<nsIContent> content = GetFocusedContentForIME();
IMEStateManager::UpdateIMEState(newState, content, this);
}
// FYI: This call might cause destroying this editor.
IMEStateManager::OnEditorInitialized(*this);
return NS_OK;
}
void
EditorBase::SetTextInputListener(TextInputListener* aTextInputListener)
{
MOZ_ASSERT(!mTextInputListener || !aTextInputListener ||
mTextInputListener == aTextInputListener);
mTextInputListener = aTextInputListener;
}
void
EditorBase::SetIMEContentObserver(IMEContentObserver* aIMEContentObserver)
{
MOZ_ASSERT(!mIMEContentObserver || !aIMEContentObserver ||
mIMEContentObserver == aIMEContentObserver);
mIMEContentObserver = aIMEContentObserver;
}
void
EditorBase::CreateEventListeners()
{
// Don't create the handler twice
if (!mEventListener) {
mEventListener = new EditorEventListener();
}
}
nsresult
EditorBase::InstallEventListeners()
{
if (NS_WARN_IF(!IsInitialized()) || NS_WARN_IF(!mEventListener)) {
return NS_ERROR_NOT_INITIALIZED;
}
// Initialize the event target.
nsCOMPtr<nsIContent> rootContent = GetRoot();
NS_ENSURE_TRUE(rootContent, NS_ERROR_NOT_AVAILABLE);
mEventTarget = rootContent->GetParent();
NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_AVAILABLE);
nsresult rv = mEventListener->Connect(this);
if (mComposition) {
// Restart to handle composition with new editor contents.
mComposition->StartHandlingComposition(this);
}
return rv;
}
void
EditorBase::RemoveEventListeners()
{
if (!IsInitialized() || !mEventListener) {
return;
}
mEventListener->Disconnect();
if (mComposition) {
// Even if this is called, don't release mComposition because this is
// may be reused after reframing.
mComposition->EndHandlingComposition(this);
}
mEventTarget = nullptr;
}
bool
EditorBase::GetDesiredSpellCheckState()
{
// Check user override on this element
if (mSpellcheckCheckboxState != eTriUnset) {
return (mSpellcheckCheckboxState == eTriTrue);
}
// Check user preferences
int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1);
if (!spellcheckLevel) {
return false; // Spellchecking forced off globally
}
if (!CanEnableSpellCheck()) {
return false;
}
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
if (presShell) {
nsPresContext* context = presShell->GetPresContext();
if (context && !context->IsDynamic()) {
return false;
}
}
// Check DOM state
nsCOMPtr<nsIContent> content = GetExposedRoot();
if (!content) {
return false;
}
auto element = nsGenericHTMLElement::FromNode(content);
if (!element) {
return false;
}
if (!IsPlaintextEditor()) {
// Some of the page content might be editable and some not, if spellcheck=
// is explicitly set anywhere, so if there's anything editable on the page,
// return true and let the spellchecker figure it out.
nsCOMPtr<nsIHTMLDocument> doc = do_QueryInterface(content->GetComposedDoc());
return doc && doc->IsEditingOn();
}
return element->Spellcheck();
}
void
EditorBase::PreDestroy(bool aDestroyingFrames)
{
if (mDidPreDestroy) {
return;
}
Selection* selection = GetSelection();
if (selection) {
selection->RemoveSelectionListener(this);
}
IMEStateManager::OnEditorDestroying(*this);
// Let spellchecker clean up its observers etc. It is important not to
// actually free the spellchecker here, since the spellchecker could have
// caused flush notifications, which could have gotten here if a textbox
// is being removed. Setting the spellchecker to nullptr could free the
// object that is still in use! It will be freed when the editor is
// destroyed.
if (mInlineSpellChecker)
mInlineSpellChecker->Cleanup(aDestroyingFrames);
// tell our listeners that the doc is going away
NotifyDocumentListeners(eDocumentToBeDestroyed);
// Unregister event listeners
RemoveEventListeners();
// If this editor is still hiding the caret, we need to restore it.
HideCaret(false);
mActionListeners.Clear();
mEditorObservers.Clear();
mDocStateListeners.Clear();
mInlineSpellChecker = nullptr;
mTextServicesDocument = nullptr;
mTextInputListener = nullptr;
mSpellcheckCheckboxState = eTriUnset;
mRootElement = nullptr;
// Transaction may grab this instance. Therefore, they should be released
// here for stopping the circular reference with this instance.
if (mTransactionManager) {
DebugOnly<bool> disabledUndoRedo = DisableUndoRedo();
NS_WARNING_ASSERTION(disabledUndoRedo,
"Failed to disable undo/redo transactions");
mTransactionManager = nullptr;
}
mDidPreDestroy = true;
}
NS_IMETHODIMP
EditorBase::GetFlags(uint32_t* aFlags)
{
// NOTE: If you need to override this method, you need to make Flags()
// virtual.
*aFlags = Flags();
return NS_OK;
}
NS_IMETHODIMP
EditorBase::SetFlags(uint32_t aFlags)
{
if (mFlags == aFlags) {
return NS_OK;
}
bool spellcheckerWasEnabled = CanEnableSpellCheck();
mFlags = aFlags;
if (!IsInitialized()) {
// If we're initializing, we shouldn't do anything now.
// SetFlags() will be called by PostCreate(),
// we should synchronize some stuff for the flags at that time.
return NS_OK;
}
// The flag change may cause the spellchecker state change
if (CanEnableSpellCheck() != spellcheckerWasEnabled) {
SyncRealTimeSpell();
}
// If this is called from PostCreate(), it will update the IME state if it's
// necessary.
if (!mDidPostCreate) {
return NS_OK;
}
// Might be changing editable state, so, we need to reset current IME state
// if we're focused and the flag change causes IME state change.
nsCOMPtr<nsIContent> focusedContent = GetFocusedContent();
if (focusedContent) {
IMEState newState;
nsresult rv = GetPreferredIMEState(&newState);
if (NS_SUCCEEDED(rv)) {
// NOTE: When the enabled state isn't going to be modified, this method
// is going to do nothing.
nsCOMPtr<nsIContent> content = GetFocusedContentForIME();
IMEStateManager::UpdateIMEState(newState, content, this);
}
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable)
{
NS_ENSURE_ARG_POINTER(aIsSelectionEditable);
*aIsSelectionEditable = IsSelectionEditable();
return NS_OK;
}
bool
EditorBase::IsSelectionEditable()
{
// get current selection
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return false;
}
if (!mIsHTMLEditorClass) {
// XXX we just check that the anchor node is editable at the moment
// we should check that all nodes in the selection are editable
nsCOMPtr<nsINode> anchorNode = selection->GetAnchorNode();
return anchorNode && IsEditable(anchorNode);
}
nsINode* anchorNode = selection->GetAnchorNode();
nsINode* focusNode = selection->GetFocusNode();
if (!anchorNode || !focusNode) {
return false;
}
// Per the editing spec as of June 2012: we have to have a selection whose
// start and end nodes are editable, and which share an ancestor editing
// host. (Bug 766387.)
bool isSelectionEditable = selection->RangeCount() &&
anchorNode->IsEditable() &&
focusNode->IsEditable();
if (!isSelectionEditable) {
return false;
}
nsINode* commonAncestor =
selection->GetAnchorFocusRange()->GetCommonAncestor();
while (commonAncestor && !commonAncestor->IsEditable()) {
commonAncestor = commonAncestor->GetParentNode();
}
// If there is no editable common ancestor, return false.
return !!commonAncestor;
}
NS_IMETHODIMP
EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable)
{
NS_ENSURE_ARG_POINTER(aIsDocumentEditable);
nsCOMPtr<nsIDocument> doc = GetDocument();
*aIsDocumentEditable = doc && IsModifiable();
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetDocument(nsIDocument** aDoc)
{
NS_IF_ADDREF(*aDoc = mDocument);
return *aDoc ? NS_OK : NS_ERROR_NOT_INITIALIZED;
}
already_AddRefed<nsIWidget>
EditorBase::GetWidget()
{
nsCOMPtr<nsIPresShell> ps = GetPresShell();
NS_ENSURE_TRUE(ps, nullptr);
nsPresContext* pc = ps->GetPresContext();
NS_ENSURE_TRUE(pc, nullptr);
nsCOMPtr<nsIWidget> widget = pc->GetRootWidget();
NS_ENSURE_TRUE(widget.get(), nullptr);
return widget.forget();
}
NS_IMETHODIMP
EditorBase::GetContentsMIMEType(char** aContentsMIMEType)
{
NS_ENSURE_ARG_POINTER(aContentsMIMEType);
*aContentsMIMEType = ToNewCString(mContentMIMEType);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::SetContentsMIMEType(const char* aContentsMIMEType)
{
mContentMIMEType.Assign(aContentsMIMEType ? aContentsMIMEType : "");
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetSelectionController(nsISelectionController** aSel)
{
NS_ENSURE_TRUE(aSel, NS_ERROR_NULL_POINTER);
*aSel = nullptr; // init out param
nsCOMPtr<nsISelectionController> selCon = GetSelectionController();
if (NS_WARN_IF(!selCon)) {
return NS_ERROR_NOT_INITIALIZED;
}
selCon.forget(aSel);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::DeleteSelection(EDirection aAction,
EStripWrappers aStripWrappers)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::GetSelection(Selection** aSelection)
{
return GetSelection(SelectionType::eNormal, aSelection);
}
nsresult
EditorBase::GetSelection(SelectionType aSelectionType,
Selection** aSelection) const
{
NS_ENSURE_TRUE(aSelection, NS_ERROR_NULL_POINTER);
*aSelection = nullptr;
nsISelectionController* selcon = GetSelectionController();
if (!selcon) {
return NS_ERROR_NOT_INITIALIZED;
}
RefPtr<Selection> selection =
selcon->GetSelection(ToRawSelectionType(aSelectionType));
if (!selection) {
return NS_ERROR_INVALID_ARG;
}
selection.forget(aSelection);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::DoTransaction(nsITransaction* aTxn)
{
return DoTransaction(nullptr, aTxn);
}
nsresult
EditorBase::DoTransaction(Selection* aSelection, nsITransaction* aTxn)
{
if (mPlaceholderBatch && !mPlaceholderTransaction) {
mPlaceholderTransaction =
PlaceholderTransaction::Create(*this, mPlaceholderName, std::move(mSelState));
MOZ_ASSERT(mSelState.isNothing());
// We will recurse, but will not hit this case in the nested call
DoTransaction(mPlaceholderTransaction);
if (mTransactionManager) {
nsCOMPtr<nsITransaction> topTransaction =
mTransactionManager->PeekUndoStack();
nsCOMPtr<nsIAbsorbingTransaction> topAbsorbingTransaction =
do_QueryInterface(topTransaction);
if (topAbsorbingTransaction) {
RefPtr<PlaceholderTransaction> topPlaceholderTransaction =
topAbsorbingTransaction->AsPlaceholderTransaction();
if (topPlaceholderTransaction) {
// there is a placeholder transaction on top of the undo stack. It
// is either the one we just created, or an earlier one that we are
// now merging into. From here on out remember this placeholder
// instead of the one we just created.
mPlaceholderTransaction = topPlaceholderTransaction;
}
}
}
}
if (aTxn) {
// XXX: Why are we doing selection specific batching stuff here?
// XXX: Most entry points into the editor have auto variables that
// XXX: should trigger Begin/EndUpdateViewBatch() calls that will make
// XXX: these selection batch calls no-ops.
// XXX:
// XXX: I suspect that this was placed here to avoid multiple
// XXX: selection changed notifications from happening until after
// XXX: the transaction was done. I suppose that can still happen
// XXX: if an embedding application called DoTransaction() directly
// XXX: to pump its own transactions through the system, but in that
// XXX: case, wouldn't we want to use Begin/EndUpdateViewBatch() or
// XXX: its auto equivalent AutoUpdateViewBatch to ensure that
// XXX: selection listeners have access to accurate frame data?
// XXX:
// XXX: Note that if we did add Begin/EndUpdateViewBatch() calls
// XXX: we will need to make sure that they are disabled during
// XXX: the init of the editor for text widgets to avoid layout
// XXX: re-entry during initial reflow. - kin
// get the selection and start a batch change
RefPtr<Selection> selection = aSelection ? aSelection : GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
SelectionBatcher selectionBatcher(selection);
nsresult rv;
if (mTransactionManager) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
rv = transactionManager->DoTransaction(aTxn);
} else {
rv = aTxn->DoTransaction();
}
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
DoAfterDoTransaction(aTxn);
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::EnableUndo(bool aEnable)
{
// XXX Should we return NS_ERROR_FAILURE if EdnableUndoRedo() or
// DisableUndoRedo() returns false?
if (aEnable) {
DebugOnly<bool> enabledUndoRedo = EnableUndoRedo();
NS_WARNING_ASSERTION(enabledUndoRedo,
"Failed to enable undo/redo transactions");
return NS_OK;
}
DebugOnly<bool> disabledUndoRedo = DisableUndoRedo();
NS_WARNING_ASSERTION(disabledUndoRedo,
"Failed to disable undo/redo transactions");
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetTransactionManager(nsITransactionManager** aTransactionManager)
{
if (NS_WARN_IF(!aTransactionManager)) {
return NS_ERROR_INVALID_ARG;
}
if (NS_WARN_IF(!mTransactionManager)) {
return NS_ERROR_FAILURE;
}
NS_IF_ADDREF(*aTransactionManager = mTransactionManager);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::Undo(uint32_t aCount)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::CanUndo(bool* aIsEnabled,
bool* aCanUndo)
{
if (NS_WARN_IF(!aIsEnabled) || NS_WARN_IF(!aCanUndo)) {
return NS_ERROR_INVALID_ARG;
}
*aCanUndo = CanUndo();
*aIsEnabled = IsUndoRedoEnabled();
return NS_OK;
}
NS_IMETHODIMP
EditorBase::Redo(uint32_t aCount)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::CanRedo(bool* aIsEnabled, bool* aCanRedo)
{
if (NS_WARN_IF(!aIsEnabled) || NS_WARN_IF(!aCanRedo)) {
return NS_ERROR_INVALID_ARG;
}
*aCanRedo = CanRedo();
*aIsEnabled = IsUndoRedoEnabled();
return NS_OK;
}
NS_IMETHODIMP
EditorBase::BeginTransaction()
{
BeginTransactionInternal();
return NS_OK;
}
void
EditorBase::BeginTransactionInternal()
{
BeginUpdateViewBatch();
if (mTransactionManager) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
transactionManager->BeginBatch(nullptr);
}
}
NS_IMETHODIMP
EditorBase::EndTransaction()
{
EndTransactionInternal();
return NS_OK;
}
void
EditorBase::EndTransactionInternal()
{
if (mTransactionManager) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
transactionManager->EndBatch(false);
}
EndUpdateViewBatch();
}
void
EditorBase::BeginPlaceholderTransaction(nsAtom* aTransactionName)
{
MOZ_ASSERT(mPlaceholderBatch >= 0, "negative placeholder batch count!");
if (!mPlaceholderBatch) {
NotifyEditorObservers(eNotifyEditorObserversOfBefore);
// time to turn on the batch
BeginUpdateViewBatch();
mPlaceholderTransaction = nullptr;
mPlaceholderName = aTransactionName;
RefPtr<Selection> selection = GetSelection();
if (selection) {
mSelState.emplace();
mSelState->SaveSelection(selection);
// Composition transaction can modify multiple nodes and it merges text
// node for ime into single text node.
// So if current selection is into IME text node, it might be failed
// to restore selection by UndoTransaction.
// So we need update selection by range updater.
if (mPlaceholderName == nsGkAtoms::IMETxnName) {
mRangeUpdater.RegisterSelectionState(*mSelState);
}
}
}
mPlaceholderBatch++;
}
void
EditorBase::EndPlaceholderTransaction()
{
MOZ_ASSERT(mPlaceholderBatch > 0,
"zero or negative placeholder batch count when ending batch!");
if (mPlaceholderBatch == 1) {
RefPtr<Selection> selection = GetSelection();
// By making the assumption that no reflow happens during the calls
// to EndUpdateViewBatch and ScrollSelectionIntoView, we are able to
// allow the selection to cache a frame offset which is used by the
// caret drawing code. We only enable this cache here; at other times,
// we have no way to know whether reflow invalidates it
// See bugs 35296 and 199412.
if (selection) {
selection->SetCanCacheFrameOffset(true);
}
// time to turn off the batch
EndUpdateViewBatch();
// make sure selection is in view
// After ScrollSelectionIntoView(), the pending notifications might be
// flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
ScrollSelectionIntoView(false);
// cached for frame offset are Not available now
if (selection) {
selection->SetCanCacheFrameOffset(false);
}
if (mSelState) {
// we saved the selection state, but never got to hand it to placeholder
// (else we ould have nulled out this pointer), so destroy it to prevent leaks.
if (mPlaceholderName == nsGkAtoms::IMETxnName) {
mRangeUpdater.DropSelectionState(*mSelState);
}
mSelState.reset();
}
// We might have never made a placeholder if no action took place.
if (mPlaceholderTransaction) {
mPlaceholderTransaction->EndPlaceHolderBatch();
// notify editor observers of action but if composing, it's done by
// compositionchange event handler.
if (!mComposition) {
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
}
mPlaceholderTransaction = nullptr;
} else {
NotifyEditorObservers(eNotifyEditorObserversOfCancel);
}
}
mPlaceholderBatch--;
}
NS_IMETHODIMP
EditorBase::SetShouldTxnSetSelection(bool aShould)
{
MakeThisAllowTransactionsToChangeSelection(aShould);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
// XXX: The rule system should tell us which node to select all on (ie, the
// root, or the body)
NS_IMETHODIMP
EditorBase::SelectAll()
{
// XXX Why doesn't this check if the document is alive?
if (!IsInitialized()) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = SelectAllInternal();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
EditorBase::SelectAllInternal()
{
MOZ_ASSERT(IsInitialized());
CommitComposition();
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
// XXX Do we need to keep handling after committing composition causes moving
// focus to different element? Although TextEditor has independent
// selection, so, we may not see any odd behavior even in such case.
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
nsresult rv = SelectEntireDocument(selection);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::BeginningOfDocument()
{
// XXX Why doesn't this check if the document is alive?
if (!IsInitialized()) {
return NS_ERROR_NOT_INITIALIZED;
}
// get the selection
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
// get the root element
dom::Element* rootElement = GetRoot();
NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
// find first editable thingy
nsCOMPtr<nsINode> firstNode = GetFirstEditableNode(rootElement);
if (!firstNode) {
// just the root node, set selection to inside the root
return selection->Collapse(rootElement, 0);
}
if (firstNode->NodeType() == nsINode::TEXT_NODE) {
// If firstNode is text, set selection to beginning of the text node.
return selection->Collapse(firstNode, 0);
}
// Otherwise, it's a leaf node and we set the selection just in front of it.
nsCOMPtr<nsIContent> parent = firstNode->GetParent();
if (!parent) {
return NS_ERROR_NULL_POINTER;
}
MOZ_ASSERT(parent->ComputeIndexOf(firstNode) == 0,
"How come the first node isn't the left most child in its parent?");
return selection->Collapse(parent, 0);
}
NS_IMETHODIMP
EditorBase::EndOfDocument()
{
RefPtr<Selection> selection = GetSelection();
return CollapseSelectionToEnd(selection);
}
nsresult
EditorBase::CollapseSelectionToEnd(Selection* aSelection)
{
// XXX Why doesn't this check if the document is alive?
if (NS_WARN_IF(!IsInitialized())) {
return NS_ERROR_NOT_INITIALIZED;
}
if (NS_WARN_IF(!aSelection)) {
return NS_ERROR_NULL_POINTER;
}
// get the root element
nsINode* node = GetRoot();
if (NS_WARN_IF(!node)) {
return NS_ERROR_NULL_POINTER;
}
nsINode* child = node->GetLastChild();
while (child && IsContainer(child)) {
node = child;
child = node->GetLastChild();
}
uint32_t length = node->Length();
return aSelection->Collapse(node, static_cast<int32_t>(length));
}
NS_IMETHODIMP
EditorBase::GetDocumentModified(bool* outDocModified)
{
NS_ENSURE_TRUE(outDocModified, NS_ERROR_NULL_POINTER);
int32_t modCount = 0;
GetModificationCount(&modCount);
*outDocModified = (modCount != 0);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetDocumentCharacterSet(nsACString& aCharset)
{
return GetDocumentCharsetInternal(aCharset);
}
nsresult
EditorBase::GetDocumentCharsetInternal(nsACString& aCharset) const
{
nsCOMPtr<nsIDocument> document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_UNEXPECTED;
}
document->GetDocumentCharacterSet()->Name(aCharset);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::SetDocumentCharacterSet(const nsACString& characterSet)
{
nsCOMPtr<nsIDocument> document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_UNEXPECTED;
}
// This method is scriptable, so add-ons could pass in something other
// than a canonical name.
auto encoding = Encoding::ForLabelNoReplacement(characterSet);
if (!encoding) {
return NS_ERROR_INVALID_ARG;
}
document->SetDocumentCharacterSet(WrapNotNull(encoding));
return NS_OK;
}
NS_IMETHODIMP
EditorBase::Cut()
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::CanCut(bool* aCanCut)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::Copy()
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::CanCopy(bool* aCanCut)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::CanDelete(bool* aCanDelete)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::Paste(int32_t aClipboardType)
{
nsresult rv = AsTextEditor()->PasteAsAction(aClipboardType, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::PasteTransferable(nsITransferable* aTransferable)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::CanPaste(int32_t aSelectionType, bool* aCanPaste)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::SetAttribute(Element* aElement,
const nsAString& aAttribute,
const nsAString& aValue)
{
if (NS_WARN_IF(aAttribute.IsEmpty())) {
return NS_ERROR_INVALID_ARG;
}
if (NS_WARN_IF(!aElement)) {
return NS_ERROR_INVALID_ARG;
}
RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
return SetAttributeWithTransaction(*aElement, *attribute, aValue);
}
nsresult
EditorBase::SetAttributeWithTransaction(Element& aElement,
nsAtom& aAttribute,
const nsAString& aValue)
{
RefPtr<ChangeAttributeTransaction> transaction =
ChangeAttributeTransaction::Create(aElement, aAttribute, aValue);
return DoTransaction(transaction);
}
NS_IMETHODIMP
EditorBase::GetAttributeValue(Element* aElement,
const nsAString& aAttribute,
nsAString& aResultValue,
bool* aResultIsSet)
{
NS_ENSURE_TRUE(aResultIsSet, NS_ERROR_NULL_POINTER);
*aResultIsSet = false;
if (!aElement) {
return NS_OK;
}
nsAutoString value;
aElement->GetAttribute(aAttribute, value);
if (!DOMStringIsNull(value)) {
*aResultIsSet = true;
aResultValue = value;
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::RemoveAttribute(Element* aElement,
const nsAString& aAttribute)
{
if (NS_WARN_IF(aAttribute.IsEmpty())) {
return NS_ERROR_INVALID_ARG;
}
if (NS_WARN_IF(!aElement)) {
return NS_ERROR_INVALID_ARG;
}
RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
return RemoveAttributeWithTransaction(*aElement, *attribute);
}
nsresult
EditorBase::RemoveAttributeWithTransaction(Element& aElement,
nsAtom& aAttribute)
{
// XXX If aElement doesn't have aAttribute, shouldn't we stop creating
// the transaction? Otherwise, there will be added a transaction
// which does nothing at doing undo/redo.
RefPtr<ChangeAttributeTransaction> transaction =
ChangeAttributeTransaction::CreateToRemove(aElement, aAttribute);
return DoTransaction(transaction);
}
NS_IMETHODIMP
EditorBase::MarkNodeDirty(nsINode* aNode)
{
// Mark the node dirty, but not for webpages (bug 599983)
if (!OutputsMozDirty()) {
return NS_OK;
}
if (RefPtr<Element> element = Element::FromNodeOrNull(aNode)) {
element->SetAttr(kNameSpaceID_None, nsGkAtoms::mozdirty, EmptyString(),
false);
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetInlineSpellChecker(bool autoCreate,
nsIInlineSpellChecker** aInlineSpellChecker)
{
NS_ENSURE_ARG_POINTER(aInlineSpellChecker);
if (mDidPreDestroy) {
// Don't allow people to get or create the spell checker once the editor
// is going away.
*aInlineSpellChecker = nullptr;
return autoCreate ? NS_ERROR_NOT_AVAILABLE : NS_OK;
}
// We don't want to show the spell checking UI if there are no spell check dictionaries available.
bool canSpell = mozInlineSpellChecker::CanEnableInlineSpellChecking();
if (!canSpell) {
*aInlineSpellChecker = nullptr;
return NS_ERROR_FAILURE;
}
nsresult rv;
if (!mInlineSpellChecker && autoCreate) {
mInlineSpellChecker = new mozInlineSpellChecker();
}
if (mInlineSpellChecker) {
rv = mInlineSpellChecker->Init(this);
if (NS_FAILED(rv)) {
mInlineSpellChecker = nullptr;
}
NS_ENSURE_SUCCESS(rv, rv);
}
NS_IF_ADDREF(*aInlineSpellChecker = mInlineSpellChecker);
return NS_OK;
}
void
EditorBase::SyncRealTimeSpell()
{
bool enable = GetDesiredSpellCheckState();
// Initializes mInlineSpellChecker
nsCOMPtr<nsIInlineSpellChecker> spellChecker;
GetInlineSpellChecker(enable, getter_AddRefs(spellChecker));
if (mInlineSpellChecker) {
if (!mSpellCheckerDictionaryUpdated && enable) {
mInlineSpellChecker->UpdateCurrentDictionary();
mSpellCheckerDictionaryUpdated = true;
}
// We might have a mInlineSpellChecker even if there are no dictionaries
// available since we don't destroy the mInlineSpellChecker when the last
// dictionariy is removed, but in that case spellChecker is null
mInlineSpellChecker->SetEnableRealTimeSpell(enable && spellChecker);
}
}
NS_IMETHODIMP
EditorBase::SetSpellcheckUserOverride(bool enable)
{
mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse;
SyncRealTimeSpell();
return NS_OK;
}
template<typename PT, typename CT>
already_AddRefed<Element>
EditorBase::CreateNodeWithTransaction(
nsAtom& aTagName,
const EditorDOMPointBase<PT, CT>& aPointToInsert)
{
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
// XXX We need offset at new node for mRangeUpdater. Therefore, we need
// to compute the offset now but this is expensive. So, if it's possible,
// we need to redesign mRangeUpdater as avoiding using indices.
Unused << aPointToInsert.Offset();
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eCreateNode,
nsIEditor::eNext);
RefPtr<Element> newElement;
RefPtr<CreateElementTransaction> transaction =
CreateElementTransaction::Create(*this, aTagName, aPointToInsert);
nsresult rv = DoTransaction(transaction);
if (NS_WARN_IF(NS_FAILED(rv))) {
// XXX Why do we do this even when DoTransaction() returned error?
mRangeUpdater.SelAdjCreateNode(aPointToInsert);
} else {
newElement = transaction->GetNewNode();
MOZ_ASSERT(newElement);
// If we succeeded to create and insert new element, we need to adjust
// ranges in mRangeUpdater. It currently requires offset of the new node.
// So, let's call it with original offset. Note that if aPointToInsert
// stores child node, it may not be at the offset since new element must
// be inserted before the old child. Although, mutation observer can do
// anything, but currently, we don't check it.
mRangeUpdater.SelAdjCreateNode(
EditorRawDOMPoint(aPointToInsert.GetContainer(),
aPointToInsert.Offset()));
}
if (mRules && mRules->AsHTMLEditRules() && newElement) {
Selection* selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidCreateNode(*selection, *newElement);
} else {
NS_WARNING("Selection has gone");
}
}
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidCreateNode(nsDependentAtomString(&aTagName),
newElement, rv);
}
}
return newElement.forget();
}
NS_IMETHODIMP
EditorBase::InsertNode(nsINode* aNodeToInsert,
nsINode* aContainer,
int32_t aOffset)
{
nsCOMPtr<nsIContent> contentToInsert = do_QueryInterface(aNodeToInsert);
if (NS_WARN_IF(!contentToInsert)) {
return NS_ERROR_NULL_POINTER;
}
if (NS_WARN_IF(!aContainer)) {
return NS_ERROR_NULL_POINTER;
}
int32_t offset =
aOffset < 0 ? static_cast<int32_t>(aContainer->Length()) :
std::min(aOffset, static_cast<int32_t>(aContainer->Length()));
return InsertNodeWithTransaction(*contentToInsert,
EditorRawDOMPoint(aContainer, offset));
}
template<typename PT, typename CT>
nsresult
EditorBase::InsertNodeWithTransaction(
nsIContent& aContentToInsert,
const EditorDOMPointBase<PT, CT>& aPointToInsert)
{
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
return NS_ERROR_INVALID_ARG;
}
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eInsertNode,
nsIEditor::eNext);
RefPtr<InsertNodeTransaction> transaction =
InsertNodeTransaction::Create(*this, aContentToInsert, aPointToInsert);
nsresult rv = DoTransaction(transaction);
mRangeUpdater.SelAdjInsertNode(aPointToInsert);
if (mRules && mRules->AsHTMLEditRules()) {
Selection* selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidInsertNode(*selection, aContentToInsert);
} else {
NS_WARNING("Selection has gone");
}
}
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidInsertNode(&aContentToInsert, rv);
}
}
return rv;
}
NS_IMETHODIMP
EditorBase::SplitNode(nsINode* aNode,
int32_t aOffset,
nsINode** aNewLeftNode)
{
if (NS_WARN_IF(!aNode)) {
return NS_ERROR_INVALID_ARG;
}
int32_t offset = std::min(std::max(aOffset, 0),
static_cast<int32_t>(aNode->Length()));
ErrorResult error;
nsCOMPtr<nsIContent> newNode =
SplitNodeWithTransaction(EditorRawDOMPoint(aNode, offset), error);
newNode.forget(aNewLeftNode);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
template<typename PT, typename CT>
already_AddRefed<nsIContent>
EditorBase::SplitNodeWithTransaction(
const EditorDOMPointBase<PT, CT>& aStartOfRightNode,
ErrorResult& aError)
{
if (NS_WARN_IF(!aStartOfRightNode.IsSet()) ||
NS_WARN_IF(!aStartOfRightNode.GetContainerAsContent())) {
aError.Throw(NS_ERROR_INVALID_ARG);
return nullptr;
}
MOZ_ASSERT(aStartOfRightNode.IsSetAndValid());
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eSplitNode,
nsIEditor::eNext);
// XXX Unfortunately, storing offset of the split point in
// SplitNodeTransaction is necessary for now. We should fix this
// in a follow up bug.
Unused << aStartOfRightNode.Offset();
RefPtr<SplitNodeTransaction> transaction =
SplitNodeTransaction::Create(*this, aStartOfRightNode);
aError = DoTransaction(transaction);
nsCOMPtr<nsIContent> newNode = transaction->GetNewNode();
NS_WARNING_ASSERTION(newNode, "Failed to create a new left node");
// XXX Some other transactions manage range updater by themselves.
// Why doesn't SplitNodeTransaction do it?
mRangeUpdater.SelAdjSplitNode(*aStartOfRightNode.GetContainerAsContent(),
newNode);
if (mRules && mRules->AsHTMLEditRules() && newNode) {
Selection* selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidSplitNode(*selection,
*aStartOfRightNode.GetContainer(), *newNode);
} else {
NS_WARNING("Selection has gone");
}
}
if (mInlineSpellChecker) {
RefPtr<mozInlineSpellChecker> spellChecker = mInlineSpellChecker;
spellChecker->DidSplitNode(aStartOfRightNode.GetContainer(), newNode);
}
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidSplitNode(aStartOfRightNode.GetContainer(), newNode);
}
}
if (NS_WARN_IF(aError.Failed())) {
return nullptr;
}
return newNode.forget();
}
NS_IMETHODIMP
EditorBase::JoinNodes(nsINode* aLeftNode,
nsINode* aRightNode,
nsINode*)
{
NS_ENSURE_STATE(aLeftNode && aRightNode && aLeftNode->GetParentNode());
return JoinNodesWithTransaction(*aLeftNode, *aRightNode);
}
nsresult
EditorBase::JoinNodesWithTransaction(nsINode& aLeftNode,
nsINode& aRightNode)
{
nsCOMPtr<nsINode> parent = aLeftNode.GetParentNode();
MOZ_ASSERT(parent);
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eJoinNodes,
nsIEditor::ePrevious);
// Remember some values; later used for saved selection updating.
// Find the offset between the nodes to be joined.
int32_t offset = parent->ComputeIndexOf(&aRightNode);
// Find the number of children of the lefthand node
uint32_t oldLeftNodeLen = aLeftNode.Length();
if (mRules && mRules->AsHTMLEditRules()) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->WillJoinNodes(aLeftNode, aRightNode);
}
nsresult rv = NS_OK;
RefPtr<JoinNodeTransaction> transaction =
JoinNodeTransaction::MaybeCreate(*this, aLeftNode, aRightNode);
if (transaction) {
rv = DoTransaction(transaction);
}
// XXX Some other transactions manage range updater by themselves.
// Why doesn't JoinNodeTransaction do it?
mRangeUpdater.SelAdjJoinNodes(aLeftNode, aRightNode, *parent, offset,
(int32_t)oldLeftNodeLen);
if (mRules && mRules->AsHTMLEditRules()) {
Selection* selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidJoinNodes(*selection, aLeftNode, aRightNode);
} else {
NS_WARNING("Selection has gone");
}
}
if (mInlineSpellChecker) {
RefPtr<mozInlineSpellChecker> spellChecker = mInlineSpellChecker;
spellChecker->DidJoinNodes(aLeftNode, aRightNode);
}
if (mTextServicesDocument && NS_SUCCEEDED(rv)) {
RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument;
textServicesDocument->DidJoinNodes(aLeftNode, aRightNode);
}
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidJoinNodes(&aLeftNode, &aRightNode, parent, rv);
}
}
return rv;
}
NS_IMETHODIMP
EditorBase::DeleteNode(nsINode* aNode)
{
if (NS_WARN_IF(!aNode)) {
return NS_ERROR_INVALID_ARG;
}
return DeleteNodeWithTransaction(*aNode);
}
nsresult
EditorBase::DeleteNodeWithTransaction(nsINode& aNode)
{
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eCreateNode,
nsIEditor::ePrevious);
if (mRules && mRules->AsHTMLEditRules()) {
Selection* selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->WillDeleteNode(*selection, aNode);
} else {
NS_WARNING("Selection has gone");
}
}
// FYI: DeleteNodeTransaction grabs aNode while it's alive. So, it's safe
// to refer aNode even after calling DoTransaction().
RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
DeleteNodeTransaction::MaybeCreate(*this, aNode);
nsresult rv = deleteNodeTransaction ? DoTransaction(deleteNodeTransaction) :
NS_ERROR_FAILURE;
if (mTextServicesDocument && NS_SUCCEEDED(rv)) {
RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument;
textServicesDocument->DidDeleteNode(&aNode);
}
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidDeleteNode(&aNode, rv);
}
}
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
already_AddRefed<Element>
EditorBase::ReplaceContainerWithTransactionInternal(
Element& aOldContainer,
nsAtom& aTagName,
nsAtom& aAttribute,
const nsAString& aAttributeValue,
bool aCloneAllAttributes)
{
EditorDOMPoint atOldContainer(&aOldContainer);
if (NS_WARN_IF(!atOldContainer.IsSet())) {
return nullptr;
}
RefPtr<Element> newContainer = CreateHTMLContent(&aTagName);
if (NS_WARN_IF(!newContainer)) {
return nullptr;
}
// Set or clone attribute if needed.
if (aCloneAllAttributes) {
MOZ_ASSERT(&aAttribute == nsGkAtoms::_empty);
CloneAttributesWithTransaction(*newContainer, aOldContainer);
} else if (&aAttribute != nsGkAtoms::_empty) {
nsresult rv =
newContainer->SetAttr(kNameSpaceID_None, &aAttribute, aAttributeValue,
true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
}
// Notify our internal selection state listener.
// Note: An AutoSelectionRestorer object must be created before calling this
// to initialize mRangeUpdater.
AutoReplaceContainerSelNotify selStateNotify(mRangeUpdater, &aOldContainer,
newContainer);
{
AutoTransactionsConserveSelection conserveSelection(*this);
// Move all children from the old container to the new container.
while (aOldContainer.HasChildren()) {
nsCOMPtr<nsIContent> child = aOldContainer.GetFirstChild();
if (NS_WARN_IF(!child)) {
return nullptr;
}
nsresult rv = DeleteNodeWithTransaction(*child);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
rv = InsertNodeWithTransaction(*child,
EditorRawDOMPoint(newContainer,
newContainer->Length()));
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
}
}
// Insert new container into tree.
NS_WARNING_ASSERTION(atOldContainer.IsSetAndValid(),
"The old container might be moved by mutation observer");
nsresult rv = InsertNodeWithTransaction(*newContainer, atOldContainer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
// Delete old container.
rv = DeleteNodeWithTransaction(aOldContainer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
return newContainer.forget();
}
nsresult
EditorBase::RemoveContainerWithTransaction(Element& aElement)
{
EditorDOMPoint pointToInsertChildren(&aElement);
if (NS_WARN_IF(!pointToInsertChildren.IsSet())) {
return NS_ERROR_FAILURE;
}
// Notify our internal selection state listener.
AutoRemoveContainerSelNotify selNotify(mRangeUpdater, &aElement,
pointToInsertChildren.GetContainer(),
pointToInsertChildren.Offset(),
aElement.GetChildCount());
// Move all children from aNode to its parent.
while (aElement.HasChildren()) {
nsCOMPtr<nsIContent> child = aElement.GetLastChild();
if (NS_WARN_IF(!child)) {
return NS_ERROR_FAILURE;
}
nsresult rv = DeleteNodeWithTransaction(*child);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Insert the last child before the previous last child. So, we need to
// use offset here because previous child might have been moved to
// container.
rv = InsertNodeWithTransaction(*child,
EditorRawDOMPoint(
pointToInsertChildren.GetContainer(),
pointToInsertChildren.Offset()));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
nsresult rv = DeleteNodeWithTransaction(aElement);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
already_AddRefed<Element>
EditorBase::InsertContainerWithTransactionInternal(
nsIContent& aContent,
nsAtom& aTagName,
nsAtom& aAttribute,
const nsAString& aAttributeValue)
{
EditorDOMPoint pointToInsertNewContainer(&aContent);
if (NS_WARN_IF(!pointToInsertNewContainer.IsSet())) {
return nullptr;
}
// aContent will be moved to the new container before inserting the new
// container. So, when we insert the container, the insertion point
// is before the next sibling of aContent.
// XXX If pointerToInsertNewContainer stores offset here, the offset and
// referring child node become mismatched. Although, currently this
// is not a problem since InsertNodeTransaction refers only child node.
DebugOnly<bool> advanced = pointToInsertNewContainer.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to after aContent");
// Create new container.
RefPtr<Element> newContainer = CreateHTMLContent(&aTagName);
if (NS_WARN_IF(!newContainer)) {
return nullptr;
}
// Set attribute if needed.
if (&aAttribute != nsGkAtoms::_empty) {
nsresult rv =
newContainer->SetAttr(kNameSpaceID_None, &aAttribute, aAttributeValue,
true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
}
// Notify our internal selection state listener
AutoInsertContainerSelNotify selNotify(mRangeUpdater);
// Put aNode in the new container, first.
nsresult rv = DeleteNodeWithTransaction(aContent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
{
AutoTransactionsConserveSelection conserveSelection(*this);
rv = InsertNodeWithTransaction(aContent,
EditorRawDOMPoint(newContainer, 0));
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
}
// Put the new container where aNode was.
rv = InsertNodeWithTransaction(*newContainer, pointToInsertNewContainer);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
return newContainer.forget();
}
template<typename PT, typename CT>
nsresult
EditorBase::MoveNodeWithTransaction(
nsIContent& aContent,
const EditorDOMPointBase<PT, CT>& aPointToInsert)
{
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
EditorDOMPoint oldPoint(&aContent);
if (NS_WARN_IF(!oldPoint.IsSet())) {
return NS_ERROR_FAILURE;
}
// Don't do anything if it's already in right place.
if (aPointToInsert == oldPoint) {
return NS_OK;
}
// Notify our internal selection state listener
EditorDOMPoint newPoint(aPointToInsert);
AutoMoveNodeSelNotify selNotify(mRangeUpdater, oldPoint, newPoint);
// Hold a reference so aNode doesn't go away when we remove it (bug 772282)
nsresult rv = DeleteNodeWithTransaction(aContent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Mutation event listener could break insertion point. Let's check it.
EditorRawDOMPoint pointToInsert(selNotify.ComputeInsertionPoint());
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return NS_ERROR_FAILURE;
}
// If some children have removed from the container, let's append to the
// container.
// XXX Perhaps, if mutation event listener inserts or removes some children
// but the child node referring with aPointToInsert is still available,
// we should insert aContent before it. However, we should keep
// traditional behavior for now.
if (NS_WARN_IF(!pointToInsert.IsSetAndValid())) {
pointToInsert.SetToEndOf(pointToInsert.GetContainer());
}
rv = InsertNodeWithTransaction(aContent, pointToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
void
EditorBase::MoveAllChildren(nsINode& aContainer,
const EditorRawDOMPoint& aPointToInsert,
ErrorResult& aError)
{
if (!aContainer.HasChildren()) {
return;
}
nsIContent* firstChild = aContainer.GetFirstChild();
if (NS_WARN_IF(!firstChild)) {
aError.Throw(NS_ERROR_FAILURE);
return;
}
nsIContent* lastChild = aContainer.GetLastChild();
if (NS_WARN_IF(!lastChild)) {
aError.Throw(NS_ERROR_FAILURE);
return;
}
return MoveChildren(*firstChild, *lastChild, aPointToInsert, aError);
}
void
EditorBase::MovePreviousSiblings(nsIContent& aChild,
const EditorRawDOMPoint& aPointToInsert,
ErrorResult& aError)
{
if (NS_WARN_IF(!aChild.GetParentNode())) {
aError.Throw(NS_ERROR_INVALID_ARG);
return;
}
nsIContent* firstChild = aChild.GetParentNode()->GetFirstChild();
if (NS_WARN_IF(!firstChild)) {
aError.Throw(NS_ERROR_FAILURE);
return;
}
nsIContent* lastChild =
&aChild == firstChild ? firstChild : aChild.GetPreviousSibling();
if (NS_WARN_IF(!lastChild)) {
aError.Throw(NS_ERROR_FAILURE);
return;
}
return MoveChildren(*firstChild, *lastChild, aPointToInsert, aError);
}
void
EditorBase::MoveChildren(nsIContent& aFirstChild,
nsIContent& aLastChild,
const EditorRawDOMPoint& aPointToInsert,
ErrorResult& aError)
{
nsCOMPtr<nsINode> oldContainer = aFirstChild.GetParentNode();
if (NS_WARN_IF(oldContainer != aLastChild.GetParentNode()) ||
NS_WARN_IF(!aPointToInsert.IsSet()) ||
NS_WARN_IF(!aPointToInsert.CanContainerHaveChildren())) {
aError.Throw(NS_ERROR_INVALID_ARG);
return;
}
// First, store all children which should be moved to the new container.
AutoTArray<nsCOMPtr<nsIContent>, 10> children;
for (nsIContent* child = &aFirstChild;
child;
child = child->GetNextSibling()) {
children.AppendElement(child);
if (child == &aLastChild) {
break;
}
}
if (NS_WARN_IF(children.LastElement() != &aLastChild)) {
aError.Throw(NS_ERROR_INVALID_ARG);
return;
}
nsCOMPtr<nsINode> newContainer = aPointToInsert.GetContainer();
nsCOMPtr<nsIContent> nextNode = aPointToInsert.GetChild();
for (size_t i = children.Length(); i > 0; --i) {
nsCOMPtr<nsIContent>& child = children[i - 1];
if (child->GetParentNode() != oldContainer) {
// If the child has been moved to different container, we shouldn't
// touch it.
continue;
}
oldContainer->RemoveChild(*child, aError);
if (NS_WARN_IF(aError.Failed())) {
return;
}
if (nextNode) {
// If we're not appending the children to the new container, we should
// check if referring next node of insertion point is still in the new
// container.
EditorRawDOMPoint pointToInsert(nextNode);
if (NS_WARN_IF(!pointToInsert.IsSet()) ||
NS_WARN_IF(pointToInsert.GetContainer() != newContainer)) {
// The next node of insertion point has been moved by mutation observer.
// Let's stop moving the remaining nodes.
// XXX Or should we move remaining children after the last moved child?
aError.Throw(NS_ERROR_FAILURE);
return;
}
}
newContainer->InsertBefore(*child, nextNode, aError);
if (NS_WARN_IF(aError.Failed())) {
return;
}
// If the child was inserted or appended properly, the following children
// should be inserted before it. Otherwise, keep using current position.
if (child->GetParentNode() == newContainer) {
nextNode = child;
}
}
}
NS_IMETHODIMP
EditorBase::AddEditorObserver(nsIEditorObserver* aObserver)
{
// we don't keep ownership of the observers. They must
// remove themselves as observers before they are destroyed.
NS_ENSURE_TRUE(aObserver, NS_ERROR_NULL_POINTER);
// Make sure the listener isn't already on the list
if (!mEditorObservers.Contains(aObserver)) {
mEditorObservers.AppendElement(*aObserver);
NS_WARNING_ASSERTION(mEditorObservers.Length() != 1,
"nsIEditorObserver installed, this editor becomes slower");
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::RemoveEditorObserver(nsIEditorObserver* aObserver)
{
NS_ENSURE_TRUE(aObserver, NS_ERROR_FAILURE);
NS_WARNING_ASSERTION(mEditorObservers.Length() != 1,
"All nsIEditorObservers have been removed, this editor becomes faster");
mEditorObservers.RemoveElement(aObserver);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::NotifySelectionChanged(nsIDocument* aDocument,
Selection* aSelection,
int16_t aReason)
{
if (NS_WARN_IF(!aDocument) || NS_WARN_IF(!aSelection)) {
return NS_ERROR_INVALID_ARG;
}
if (mTextInputListener) {
RefPtr<TextInputListener> textInputListener = mTextInputListener;
textInputListener->OnSelectionChange(*aSelection, aReason);
}
if (mIMEContentObserver) {
RefPtr<IMEContentObserver> observer = mIMEContentObserver;
observer->OnSelectionChange(*aSelection);
}
return NS_OK;
}
class EditorInputEventDispatcher final : public Runnable
{
public:
EditorInputEventDispatcher(EditorBase* aEditorBase,
nsIContent* aTarget,
bool aIsComposing)
: Runnable("EditorInputEventDispatcher")
, mEditorBase(aEditorBase)
, mTarget(aTarget)
, mIsComposing(aIsComposing)
{
}
NS_IMETHOD Run() override
{
// Note that we don't need to check mDispatchInputEvent here. We need
// to check it only when the editor requests to dispatch the input event.
if (!mTarget->IsInComposedDoc()) {
return NS_OK;
}
nsCOMPtr<nsIPresShell> ps = mEditorBase->GetPresShell();
if (!ps) {
return NS_OK;
}
nsCOMPtr<nsIWidget> widget = mEditorBase->GetWidget();
if (!widget) {
return NS_OK;
}
// Even if the change is caused by untrusted event, we need to dispatch
// trusted input event since it's a fact.
InternalEditorInputEvent inputEvent(true, eEditorInput, widget);
inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000);
inputEvent.mIsComposing = mIsComposing;
nsEventStatus status = nsEventStatus_eIgnore;
nsresult rv =
ps->HandleEventWithTarget(&inputEvent, nullptr, mTarget, &status);
NS_ENSURE_SUCCESS(rv, NS_OK); // print the warning if error
return NS_OK;
}
private:
RefPtr<EditorBase> mEditorBase;
nsCOMPtr<nsIContent> mTarget;
bool mIsComposing;
};
void
EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification)
{
switch (aNotification) {
case eNotifyEditorObserversOfEnd:
mIsInEditSubAction = false;
if (mTextInputListener) {
RefPtr<TextInputListener> listener = mTextInputListener;
listener->OnEditActionHandled();
}
if (mIMEContentObserver) {
RefPtr<IMEContentObserver> observer = mIMEContentObserver;
observer->OnEditActionHandled();
}
if (!mEditorObservers.IsEmpty()) {
// Copy the observers since EditAction()s can modify mEditorObservers.
AutoEditorObserverArray observers(mEditorObservers);
for (auto& observer : observers) {
observer->EditAction();
}
}
if (!mDispatchInputEvent) {
return;
}
FireInputEvent();
break;
case eNotifyEditorObserversOfBefore:
if (NS_WARN_IF(mIsInEditSubAction)) {
break;
}
mIsInEditSubAction = true;
if (mIMEContentObserver) {
RefPtr<IMEContentObserver> observer = mIMEContentObserver;
observer->BeforeEditAction();
}
break;
case eNotifyEditorObserversOfCancel:
mIsInEditSubAction = false;
if (mIMEContentObserver) {
RefPtr<IMEContentObserver> observer = mIMEContentObserver;
observer->CancelEditAction();
}
break;
default:
MOZ_CRASH("Handle all notifications here");
break;
}
}
void
EditorBase::FireInputEvent()
{
// We don't need to dispatch multiple input events if there is a pending
// input event. However, it may have different event target. If we resolved
// this issue, we need to manage the pending events in an array. But it's
// overwork. We don't need to do it for the very rare case.
nsCOMPtr<nsIContent> target = GetInputEventTargetContent();
NS_ENSURE_TRUE_VOID(target);
// NOTE: Don't refer IsIMEComposing() because it returns false even before
// compositionend. However, DOM Level 3 Events defines it should be
// true after compositionstart and before compositionend.
nsContentUtils::AddScriptRunner(
new EditorInputEventDispatcher(this, target, !!GetComposition()));
}
NS_IMETHODIMP
EditorBase::AddEditActionListener(nsIEditActionListener* aListener)
{
NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
// If given edit action listener is text services document for the inline
// spell checker, store it as reference of concrete class for performance
// reason.
if (mInlineSpellChecker) {
EditorSpellCheck* editorSpellCheck =
mInlineSpellChecker->GetEditorSpellCheck();
if (editorSpellCheck) {
mozSpellChecker* spellChecker = editorSpellCheck->GetSpellChecker();
if (spellChecker) {
TextServicesDocument* textServicesDocument =
spellChecker->GetTextServicesDocument();
if (static_cast<nsIEditActionListener*>(textServicesDocument) ==
aListener) {
mTextServicesDocument = textServicesDocument;
return NS_OK;
}
}
}
}
// Make sure the listener isn't already on the list
if (!mActionListeners.Contains(aListener)) {
mActionListeners.AppendElement(*aListener);
NS_WARNING_ASSERTION(mActionListeners.Length() != 1,
"nsIEditActionListener installed, this editor becomes slower");
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::RemoveEditActionListener(nsIEditActionListener* aListener)
{
NS_ENSURE_TRUE(aListener, NS_ERROR_FAILURE);
if (static_cast<nsIEditActionListener*>(mTextServicesDocument) == aListener) {
mTextServicesDocument = nullptr;
return NS_OK;
}
NS_WARNING_ASSERTION(mActionListeners.Length() != 1,
"All nsIEditActionListeners have been removed, this editor becomes faster");
mActionListeners.RemoveElement(aListener);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::AddDocumentStateListener(nsIDocumentStateListener* aListener)
{
NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
if (!mDocStateListeners.Contains(aListener)) {
mDocStateListeners.AppendElement(*aListener);
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::RemoveDocumentStateListener(nsIDocumentStateListener* aListener)
{
NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
mDocStateListeners.RemoveElement(aListener);
return NS_OK;
}
NS_IMETHODIMP
EditorBase::OutputToString(const nsAString& aFormatType,
uint32_t aFlags,
nsAString& aOutputString)
{
// these should be implemented by derived classes.
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
EditorBase::DumpContentTree()
{
#ifdef DEBUG
if (mRootElement) {
mRootElement->List(stdout);
}
#endif
return NS_OK;
}
NS_IMETHODIMP
EditorBase::DebugDumpContent()
{
#ifdef DEBUG
nsCOMPtr<nsIDocument> document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_NOT_INITIALIZED;
}
Element* body = document->GetBody();
if (body) {
body->List();
}
#endif
return NS_OK;
}
NS_IMETHODIMP
EditorBase::DebugUnitTests(int32_t* outNumTests,
int32_t* outNumTestsFailed)
{
MOZ_ASSERT_UNREACHABLE("This should never get called. Overridden by "
"subclasses");
return NS_OK;
}
bool
EditorBase::ArePreservingSelection()
{
return !(mSavedSel.IsEmpty());
}
void
EditorBase::PreserveSelectionAcrossActions(Selection* aSel)
{
mSavedSel.SaveSelection(aSel);
mRangeUpdater.RegisterSelectionState(mSavedSel);
}
nsresult
EditorBase::RestorePreservedSelection(Selection* aSel)
{
if (mSavedSel.IsEmpty()) {
return NS_ERROR_FAILURE;
}
mSavedSel.RestoreSelection(aSel);
StopPreservingSelection();
return NS_OK;
}
void
EditorBase::StopPreservingSelection()
{
mRangeUpdater.DropSelectionState(mSavedSel);
mSavedSel.MakeEmpty();
}
NS_IMETHODIMP
EditorBase::ForceCompositionEnd()
{
return CommitComposition();
}
nsresult
EditorBase::CommitComposition()
{
nsPresContext* pc = GetPresContext();
if (!pc) {
return NS_ERROR_NOT_AVAILABLE;
}
return mComposition ?
IMEStateManager::NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, pc) : NS_OK;
}
nsresult
EditorBase::GetPreferredIMEState(IMEState* aState)
{
NS_ENSURE_ARG_POINTER(aState);
aState->mEnabled = IMEState::ENABLED;
aState->mOpen = IMEState::DONT_CHANGE_OPEN_STATE;
if (IsReadonly() || IsDisabled()) {
aState->mEnabled = IMEState::DISABLED;
return NS_OK;
}
nsCOMPtr<nsIContent> content = GetRoot();
NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
nsIFrame* frame = content->GetPrimaryFrame();
NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
switch (frame->StyleUIReset()->mIMEMode) {
case NS_STYLE_IME_MODE_AUTO:
if (IsPasswordEditor())
aState->mEnabled = IMEState::PASSWORD;
break;
case NS_STYLE_IME_MODE_DISABLED:
// we should use password state for |ime-mode: disabled;|.
aState->mEnabled = IMEState::PASSWORD;
break;
case NS_STYLE_IME_MODE_ACTIVE:
aState->mOpen = IMEState::OPEN;
break;
case NS_STYLE_IME_MODE_INACTIVE:
aState->mOpen = IMEState::CLOSED;
break;
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetComposing(bool* aResult)
{
NS_ENSURE_ARG_POINTER(aResult);
*aResult = IsIMEComposing();
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetRootElement(Element** aRootElement)
{
NS_ENSURE_ARG_POINTER(aRootElement);
NS_ENSURE_TRUE(mRootElement, NS_ERROR_NOT_AVAILABLE);
RefPtr<Element> rootElement = mRootElement;
rootElement.forget(aRootElement);
return NS_OK;
}
void
EditorBase::OnStartToHandleTopLevelEditSubAction(
EditSubAction aEditSubAction,
nsIEditor::EDirection aDirection)
{
mTopLevelEditSubAction = aEditSubAction;
mDirection = aDirection;
}
void
EditorBase::OnEndHandlingTopLevelEditSubAction()
{
mTopLevelEditSubAction = EditSubAction::eNone;
mDirection = eNone;
}
NS_IMETHODIMP
EditorBase::CloneAttribute(const nsAString& aAttribute,
Element* aDestElement,
Element* aSourceElement)
{
NS_ENSURE_TRUE(aDestElement && aSourceElement, NS_ERROR_NULL_POINTER);
if (NS_WARN_IF(aAttribute.IsEmpty())) {
return NS_ERROR_FAILURE;
}
RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
return CloneAttributeWithTransaction(*attribute, *aDestElement, *aSourceElement);
}
nsresult
EditorBase::CloneAttributeWithTransaction(nsAtom& aAttribute,
Element& aDestElement,
Element& aSourceElement)
{
nsAutoString attrValue;
if (aSourceElement.GetAttr(kNameSpaceID_None, &aAttribute, attrValue)) {
return SetAttributeWithTransaction(aDestElement, aAttribute, attrValue);
}
return RemoveAttributeWithTransaction(aDestElement, aAttribute);
}
/**
* @param aDest Must be a DOM element.
* @param aSource Must be a DOM element.
*/
NS_IMETHODIMP
EditorBase::CloneAttributes(Element* aDestElement,
Element* aSourceElement)
{
if (NS_WARN_IF(!aDestElement) || NS_WARN_IF(!aSourceElement)) {
return NS_ERROR_INVALID_ARG;
}
CloneAttributesWithTransaction(*aDestElement, *aSourceElement);
return NS_OK;
}
void
EditorBase::CloneAttributesWithTransaction(Element& aDestElement,
Element& aSourceElement)
{
AutoPlaceholderBatch beginBatching(this);
// Use transaction system for undo only if destination is already in the
// document
Element* rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return;
}
OwningNonNull<Element> destElement(aDestElement);
OwningNonNull<Element> sourceElement(aSourceElement);
bool isDestElementInBody = rootElement->Contains(destElement);
// Clear existing attributes
RefPtr<nsDOMAttributeMap> destAttributes = destElement->Attributes();
while (RefPtr<Attr> attr = destAttributes->Item(0)) {
if (isDestElementInBody) {
RemoveAttributeWithTransaction(destElement,
*attr->NodeInfo()->NameAtom());
} else {
destElement->UnsetAttr(kNameSpaceID_None, attr->NodeInfo()->NameAtom(),
true);
}
}
// Set just the attributes that the source element has
RefPtr<nsDOMAttributeMap> sourceAttributes = sourceElement->Attributes();
uint32_t sourceCount = sourceAttributes->Length();
for (uint32_t i = 0; i < sourceCount; i++) {
RefPtr<Attr> attr = sourceAttributes->Item(i);
nsAutoString value;
attr->GetValue(value);
if (isDestElementInBody) {
SetAttributeOrEquivalent(destElement, attr->NodeInfo()->NameAtom(), value,
false);
} else {
// The element is not inserted in the document yet, we don't want to put
// a transaction on the UndoStack
SetAttributeOrEquivalent(destElement, attr->NodeInfo()->NameAtom(), value,
true);
}
}
}
nsresult
EditorBase::ScrollSelectionIntoView(bool aScrollToAnchor)
{
nsISelectionController* selectionController = GetSelectionController();
if (!selectionController) {
return NS_OK;
}
int16_t region = nsISelectionController::SELECTION_FOCUS_REGION;
if (aScrollToAnchor) {
region = nsISelectionController::SELECTION_ANCHOR_REGION;
}
selectionController->ScrollSelectionIntoView(
nsISelectionController::SELECTION_NORMAL,
region,
nsISelectionController::SCROLL_OVERFLOW_HIDDEN);
return NS_OK;
}
EditorRawDOMPoint
EditorBase::FindBetterInsertionPoint(const EditorRawDOMPoint& aPoint)
{
if (NS_WARN_IF(!aPoint.IsSet())) {
return aPoint;
}
MOZ_ASSERT(aPoint.IsSetAndValid());
if (aPoint.IsInTextNode()) {
// There is no "better" insertion point.
return aPoint;
}
if (!IsPlaintextEditor()) {
// We cannot find "better" insertion point in HTML editor.
// WARNING: When you add some code to find better node in HTML editor,
// you need to call this before calling InsertTextWithTransaction()
// in HTMLEditRules.
return aPoint;
}
nsCOMPtr<nsINode> root = GetRoot();
if (aPoint.GetContainer() == root) {
// In some cases, aNode is the anonymous DIV, and offset is 0. To avoid
// injecting unneeded text nodes, we first look to see if we have one
// available. In that case, we'll just adjust node and offset accordingly.
if (aPoint.IsStartOfContainer() &&
aPoint.GetContainer()->HasChildren() &&
aPoint.GetContainer()->GetFirstChild()->IsText()) {
return EditorRawDOMPoint(aPoint.GetContainer()->GetFirstChild(), 0);
}
// In some other cases, aNode is the anonymous DIV, and offset points to the
// terminating mozBR. In that case, we'll adjust aInOutNode and
// aInOutOffset to the preceding text node, if any.
if (!aPoint.IsStartOfContainer()) {
if (AsHTMLEditor()) {
// Fall back to a slow path that uses GetChildAt_Deprecated() for Thunderbird's
// plaintext editor.
nsIContent* child = aPoint.GetPreviousSiblingOfChild();
if (child && child->IsText()) {
if (NS_WARN_IF(child->Length() > INT32_MAX)) {
return aPoint;
}
return EditorRawDOMPoint(child, child->Length());
}
} else {
// If we're in a real plaintext editor, use a fast path that avoids
// calling GetChildAt_Deprecated() which may perform a linear search.
nsIContent* child = aPoint.GetContainer()->GetLastChild();
while (child) {
if (child->IsText()) {
if (NS_WARN_IF(child->Length() > INT32_MAX)) {
return aPoint;
}
return EditorRawDOMPoint(child, child->Length());
}
child = child->GetPreviousSibling();
}
}
}
}
// Sometimes, aNode is the mozBR element itself. In that case, we'll adjust
// the insertion point to the previous text node, if one exists, or to the
// parent anonymous DIV.
if (TextEditUtils::IsMozBR(aPoint.GetContainer()) &&
aPoint.IsStartOfContainer()) {
nsIContent* previousSibling = aPoint.GetContainer()->GetPreviousSibling();
if (previousSibling && previousSibling->IsText()) {
if (NS_WARN_IF(previousSibling->Length() > INT32_MAX)) {
return aPoint;
}
return EditorRawDOMPoint(previousSibling, previousSibling->Length());
}
nsINode* parentOfContainer = aPoint.GetContainer()->GetParentNode();
if (parentOfContainer && parentOfContainer == root) {
return EditorRawDOMPoint(parentOfContainer,
aPoint.GetContainerAsContent(), 0);
}
}
return aPoint;
}
nsresult
EditorBase::InsertTextWithTransaction(
nsIDocument& aDocument,
const nsAString& aStringToInsert,
const EditorRawDOMPoint& aPointToInsert,
EditorRawDOMPoint* aPointAfterInsertedString)
{
MOZ_ASSERT(ShouldHandleIMEComposition() ||
!AllowsTransactionsToChangeSelection(),
"caller must have already used AutoTransactionsConserveSelection "
"if this is not for updating composition string");
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
return NS_ERROR_INVALID_ARG;
}
MOZ_ASSERT(aPointToInsert.IsSetAndValid());
if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
if (aPointAfterInsertedString) {
*aPointAfterInsertedString = aPointToInsert;
}
return NS_OK;
}
// This method doesn't support over INT32_MAX length text since aInOutOffset
// is int32_t*.
CheckedInt<int32_t> lengthToInsert(aStringToInsert.Length());
if (NS_WARN_IF(!lengthToInsert.isValid())) {
return NS_ERROR_INVALID_ARG;
}
// In some cases, the node may be the anonymous div elemnt or a mozBR
// element. Let's try to look for better insertion point in the nearest
// text node if there is.
EditorRawDOMPoint pointToInsert = FindBetterInsertionPoint(aPointToInsert);
// If a neighboring text node already exists, use that
if (!pointToInsert.IsInTextNode()) {
nsIContent* child = nullptr;
if (!pointToInsert.IsStartOfContainer() &&
(child = pointToInsert.GetPreviousSiblingOfChild()) &&
child->IsText()) {
pointToInsert.Set(child, child->Length());
} else if (!pointToInsert.IsEndOfContainer() &&
(child = pointToInsert.GetChild()) &&
child->IsText()) {
pointToInsert.Set(child, 0);
}
}
if (ShouldHandleIMEComposition()) {
CheckedInt<int32_t> newOffset;
if (!pointToInsert.IsInTextNode()) {
// create a text node
RefPtr<nsTextNode> newNode =
EditorBase::CreateTextNode(aDocument, EmptyString());
// then we insert it into the dom tree
nsresult rv = InsertNodeWithTransaction(*newNode, pointToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
pointToInsert.Set(newNode, 0);
newOffset = lengthToInsert;
} else {
newOffset = lengthToInsert + pointToInsert.Offset();
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
}
nsresult rv =
InsertTextIntoTextNodeWithTransaction(aStringToInsert,
*pointToInsert.GetContainerAsText(),
pointToInsert.Offset());
NS_ENSURE_SUCCESS(rv, rv);
if (aPointAfterInsertedString) {
aPointAfterInsertedString->Set(pointToInsert.GetContainer(),
newOffset.value());
}
return NS_OK;
}
if (pointToInsert.IsInTextNode()) {
CheckedInt<int32_t> newOffset = lengthToInsert + pointToInsert.Offset();
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
// we are inserting text into an existing text node.
nsresult rv =
InsertTextIntoTextNodeWithTransaction(aStringToInsert,
*pointToInsert.GetContainerAsText(),
pointToInsert.Offset());
NS_ENSURE_SUCCESS(rv, rv);
if (aPointAfterInsertedString) {
aPointAfterInsertedString->Set(pointToInsert.GetContainer(),
newOffset.value());
}
return NS_OK;
}
// we are inserting text into a non-text node. first we have to create a
// textnode (this also populates it with the text)
RefPtr<nsTextNode> newNode =
EditorBase::CreateTextNode(aDocument, aStringToInsert);
// then we insert it into the dom tree
nsresult rv = InsertNodeWithTransaction(*newNode, pointToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (aPointAfterInsertedString) {
aPointAfterInsertedString->Set(newNode, lengthToInsert.value());
}
return NS_OK;
}
nsresult
EditorBase::InsertTextIntoTextNodeWithTransaction(
const nsAString& aStringToInsert,
Text& aTextNode,
int32_t aOffset,
bool aSuppressIME)
{
RefPtr<EditTransactionBase> transaction;
bool isIMETransaction = false;
RefPtr<Text> insertedTextNode = &aTextNode;
int32_t insertedOffset = aOffset;
// aSuppressIME is used when editor must insert text, yet this text is not
// part of the current IME operation. Example: adjusting whitespace around an
// IME insertion.
if (ShouldHandleIMEComposition() && !aSuppressIME) {
transaction =
CompositionTransaction::Create(*this, aStringToInsert,
aTextNode, aOffset);
isIMETransaction = true;
// All characters of the composition string will be replaced with
// aStringToInsert. So, we need to emulate to remove the composition
// string.
// FYI: The text node information in mComposition has been updated by
// CompositionTransaction::Create().
insertedTextNode = mComposition->GetContainerTextNode();
insertedOffset = mComposition->XPOffsetInTextNode();
} else {
transaction =
InsertTextTransaction::Create(*this, aStringToInsert, aTextNode, aOffset);
}
// XXX We may not need these view batches anymore. This is handled at a
// higher level now I believe.
BeginUpdateViewBatch();
nsresult rv = DoTransaction(transaction);
EndUpdateViewBatch();
if (mRules && mRules->AsHTMLEditRules() && insertedTextNode) {
Selection* selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidInsertText(*selection, *insertedTextNode,
insertedOffset, aStringToInsert);
} else {
NS_WARNING("Selection has gone");
}
}
// let listeners know what happened
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidInsertText(insertedTextNode, insertedOffset,
aStringToInsert, rv);
}
}
// Added some cruft here for bug 43366. Layout was crashing because we left
// an empty text node lying around in the document. So I delete empty text
// nodes caused by IME. I have to mark the IME transaction as "fixed", which
// means that furure IME txns won't merge with it. This is because we don't
// want future IME txns trying to put their text into a node that is no
// longer in the document. This does not break undo/redo, because all these
// txns are wrapped in a parent PlaceHolder txn, and placeholder txns are
// already savvy to having multiple ime txns inside them.
// Delete empty IME text node if there is one
if (isIMETransaction && mComposition) {
Text* textNode = mComposition->GetContainerTextNode();
if (textNode && !textNode->Length()) {
DeleteNodeWithTransaction(*textNode);
mComposition->OnTextNodeRemoved();
static_cast<CompositionTransaction*>(transaction.get())->MarkFixed();
}
}
return rv;
}
nsresult
EditorBase::SelectEntireDocument(Selection* aSelection)
{
if (!aSelection) {
return NS_ERROR_NULL_POINTER;
}
Element* rootElement = GetRoot();
if (!rootElement) {
return NS_ERROR_NOT_INITIALIZED;
}
ErrorResult errorResult;
aSelection->SelectAllChildren(*rootElement, errorResult);
return errorResult.StealNSResult();
}
nsINode*
EditorBase::GetFirstEditableNode(nsINode* aRoot)
{
MOZ_ASSERT(aRoot);
nsIContent* node = GetLeftmostChild(aRoot);
if (node && !IsEditable(node)) {
node = GetNextEditableNode(*node);
}
return (node != aRoot) ? node : nullptr;
}
nsresult
EditorBase::NotifyDocumentListeners(
TDocumentListenerNotification aNotificationType)
{
if (!mDocStateListeners.Length()) {
// Maybe there just aren't any.
return NS_OK;
}
AutoDocumentStateListenerArray listeners(mDocStateListeners);
nsresult rv = NS_OK;
switch (aNotificationType) {
case eDocumentCreated:
for (auto& listener : listeners) {
rv = listener->NotifyDocumentCreated();
if (NS_FAILED(rv)) {
break;
}
}
break;
case eDocumentToBeDestroyed:
for (auto& listener : listeners) {
rv = listener->NotifyDocumentWillBeDestroyed();
if (NS_FAILED(rv)) {
break;
}
}
break;
case eDocumentStateChanged: {
bool docIsDirty;
rv = GetDocumentModified(&docIsDirty);
NS_ENSURE_SUCCESS(rv, rv);
if (static_cast<int8_t>(docIsDirty) == mDocDirtyState) {
return NS_OK;
}
mDocDirtyState = docIsDirty;
for (auto& listener : listeners) {
rv = listener->NotifyDocumentStateChanged(mDocDirtyState);
if (NS_FAILED(rv)) {
break;
}
}
break;
}
default:
MOZ_ASSERT_UNREACHABLE("Unknown notification");
}
return rv;
}
nsresult
EditorBase::SetTextImpl(Selection& aSelection, const nsAString& aString,
Text& aCharData)
{
const uint32_t length = aCharData.Length();
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eSetText,
nsIEditor::eNext);
// Let listeners know what's up
if (!mActionListeners.IsEmpty() && length) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->WillDeleteText(&aCharData, 0, length);
}
}
// We don't support undo here, so we don't really need all of the transaction
// machinery, therefore we can run our transaction directly, breaking all of
// the rules!
ErrorResult res;
aCharData.SetData(aString, res);
nsresult rv = res.StealNSResult();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
{
// Create a nested scope to not overwrite rv from the outer scope.
DebugOnly<nsresult> rv = selection->Collapse(&aCharData, aString.Length());
NS_ASSERTION(NS_SUCCEEDED(rv),
"Selection could not be collapsed after insert");
}
mRangeUpdater.SelAdjDeleteText(&aCharData, 0, length);
mRangeUpdater.SelAdjInsertText(aCharData, 0, aString);
if (mRules && mRules->AsHTMLEditRules()) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
if (length) {
htmlEditRules->DidDeleteText(*selection, aCharData, 0, length);
}
if (!aString.IsEmpty()) {
htmlEditRules->DidInsertText(*selection, aCharData, 0, aString);
}
}
// Let listeners know what happened
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
if (length) {
listener->DidDeleteText(&aCharData, 0, length, rv);
}
if (!aString.IsEmpty()) {
listener->DidInsertText(&aCharData, 0, aString, rv);
}
}
}
return rv;
}
nsresult
EditorBase::DeleteTextWithTransaction(CharacterData& aCharData,
uint32_t aOffset,
uint32_t aLength)
{
RefPtr<DeleteTextTransaction> transaction =
DeleteTextTransaction::MaybeCreate(*this, aCharData, aOffset, aLength);
if (NS_WARN_IF(!transaction)) {
return NS_ERROR_FAILURE;
}
AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
*this, EditSubAction::eDeleteText,
nsIEditor::ePrevious);
// Let listeners know what's up
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->WillDeleteText(&aCharData, aOffset, aLength);
}
}
nsresult rv = DoTransaction(transaction);
if (mRules && mRules->AsHTMLEditRules()) {
RefPtr<Selection> selection = GetSelection();
if (selection) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidDeleteText(*selection, aCharData, aOffset, aLength);
} else {
NS_WARNING("Selection has gone");
}
}
// Let listeners know what happened
if (!mActionListeners.IsEmpty()) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->DidDeleteText(&aCharData, aOffset, aLength, rv);
}
}
return rv;
}
struct SavedRange final
{
RefPtr<Selection> mSelection;
nsCOMPtr<nsINode> mStartContainer;
nsCOMPtr<nsINode> mEndContainer;
int32_t mStartOffset;
int32_t mEndOffset;
};
void
EditorBase::DoSplitNode(const EditorDOMPoint& aStartOfRightNode,
nsIContent& aNewLeftNode,
ErrorResult& aError)
{
if (NS_WARN_IF(aError.Failed())) {
return;
}
// XXX Perhaps, aStartOfRightNode may be invalid if this is a redo
// operation after modifying DOM node with JS.
if (NS_WARN_IF(!aStartOfRightNode.IsSet())) {
aError.Throw(NS_ERROR_INVALID_ARG);
return;
}
MOZ_ASSERT(aStartOfRightNode.IsSetAndValid());
// Remember all selection points.
AutoTArray<SavedRange, 10> savedRanges;
for (SelectionType selectionType : kPresentSelectionTypes) {
SavedRange range;
range.mSelection = GetSelection(selectionType);
if (NS_WARN_IF(!range.mSelection &&
selectionType == SelectionType::eNormal)) {
aError.Throw(NS_ERROR_FAILURE);
return;
} else if (!range.mSelection) {
// For non-normal selections, skip over the non-existing ones.
continue;
}
for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) {
RefPtr<nsRange> r = range.mSelection->GetRangeAt(j);
MOZ_ASSERT(r->IsPositioned());
// XXX Looks like that SavedRange should have mStart and mEnd which
// are RangeBoundary. Then, we can avoid to compute offset here.
range.mStartContainer = r->GetStartContainer();
range.mStartOffset = r->StartOffset();
range.mEndContainer = r->GetEndContainer();
range.mEndOffset = r->EndOffset();
savedRanges.AppendElement(range);
}
}
nsCOMPtr<nsINode> parent = aStartOfRightNode.GetContainer()->GetParentNode();
if (NS_WARN_IF(!parent)) {
aError.Throw(NS_ERROR_FAILURE);
return;
}
// Fix the child before mutation observer may touch the DOM tree.
nsIContent* firstChildOfRightNode = aStartOfRightNode.GetChild();
parent->InsertBefore(aNewLeftNode, aStartOfRightNode.GetContainer(),
aError);
if (NS_WARN_IF(aError.Failed())) {
return;
}
// At this point, the existing right node has all the children. Move all
// the children which are before aStartOfRightNode.
if (!aStartOfRightNode.IsStartOfContainer()) {
// If it's a text node, just shuffle around some text
Text* rightAsText = aStartOfRightNode.GetContainerAsText();
Text* leftAsText = aNewLeftNode.GetAsText();
if (rightAsText && leftAsText) {
// Fix right node
nsAutoString leftText;
rightAsText->SubstringData(0, aStartOfRightNode.Offset(),
leftText, IgnoreErrors());
rightAsText->DeleteData(0, aStartOfRightNode.Offset(), IgnoreErrors());
// Fix left node
leftAsText->GetAsText()->SetData(leftText, IgnoreErrors());
} else {
MOZ_DIAGNOSTIC_ASSERT(!rightAsText && !leftAsText);
// Otherwise it's an interior node, so shuffle around the children. Go
// through list backwards so deletes don't interfere with the iteration.
if (!firstChildOfRightNode) {
MoveAllChildren(*aStartOfRightNode.GetContainer(),
EditorRawDOMPoint(&aNewLeftNode, 0), aError);
NS_WARNING_ASSERTION(!aError.Failed(),
"Failed to move all children from the right node to the left node");
} else if (NS_WARN_IF(aStartOfRightNode.GetContainer() !=
firstChildOfRightNode->GetParentNode())) {
// firstChildOfRightNode has been moved by mutation observer.
// In this case, we what should we do? Use offset? But we cannot
// check if the offset is still expected.
} else {
MovePreviousSiblings(*firstChildOfRightNode,
EditorRawDOMPoint(&aNewLeftNode, 0), aError);
NS_WARNING_ASSERTION(!aError.Failed(),
"Failed to move some children from the right node to the left node");
}
}
}
// XXX Why do we ignore an error while moving nodes from the right node to
// the left node?
aError.SuppressException();
// Handle selection
nsCOMPtr<nsIPresShell> ps = GetPresShell();
if (ps) {
ps->FlushPendingNotifications(FlushType::Frames);
}
NS_WARNING_ASSERTION(!Destroyed(),
"The editor is destroyed during splitting a node");
bool allowedTransactionsToChangeSelection =
AllowsTransactionsToChangeSelection();
RefPtr<Selection> previousSelection;
for (size_t i = 0; i < savedRanges.Length(); ++i) {
// Adjust the selection if needed.
SavedRange& range = savedRanges[i];
// If we have not seen the selection yet, clear all of its ranges.
if (range.mSelection != previousSelection) {
range.mSelection->RemoveAllRanges(aError);
if (NS_WARN_IF(aError.Failed())) {
return;
}
previousSelection = range.mSelection;
}
// XXX Looks like that we don't need to modify normal selection here
// because selection will be modified by the caller if
// AllowsTransactionsToChangeSelection() will return true.
if (allowedTransactionsToChangeSelection &&
range.mSelection->Type() == SelectionType::eNormal) {
// If the editor should adjust the selection, don't bother restoring
// the ranges for the normal selection here.
continue;
}
// Split the selection into existing node and new node.
if (range.mStartContainer == aStartOfRightNode.GetContainer()) {
if (static_cast<uint32_t>(range.mStartOffset) <
aStartOfRightNode.Offset()) {
range.mStartContainer = &aNewLeftNode;
} else {
range.mStartOffset -= aStartOfRightNode.Offset();
}
}
if (range.mEndContainer == aStartOfRightNode.GetContainer()) {
if (static_cast<uint32_t>(range.mEndOffset) <
aStartOfRightNode.Offset()) {
range.mEndContainer = &aNewLeftNode;
} else {
range.mEndOffset -= aStartOfRightNode.Offset();
}
}
RefPtr<nsRange> newRange;
nsresult rv = nsRange::CreateRange(range.mStartContainer,
range.mStartOffset,
range.mEndContainer,
range.mEndOffset,
getter_AddRefs(newRange));
if (NS_WARN_IF(NS_FAILED(rv))) {
aError.Throw(rv);
return;
}
range.mSelection->AddRange(*newRange, aError);
if (NS_WARN_IF(aError.Failed())) {
return;
}
}
// We don't need to set selection here because the caller should do that
// in any case.
}
nsresult
EditorBase::DoJoinNodes(nsINode* aNodeToKeep,
nsINode* aNodeToJoin,
nsINode* aParent)
{
MOZ_ASSERT(aNodeToKeep);
MOZ_ASSERT(aNodeToJoin);
MOZ_ASSERT(aParent);
uint32_t firstNodeLength = aNodeToJoin->Length();
int32_t joinOffset;
GetNodeLocation(aNodeToJoin, &joinOffset);
int32_t keepOffset;
nsINode* parent = GetNodeLocation(aNodeToKeep, &keepOffset);
// Remember all selection points.
AutoTArray<SavedRange, 10> savedRanges;
for (SelectionType selectionType : kPresentSelectionTypes) {
SavedRange range;
range.mSelection = GetSelection(selectionType);
if (selectionType == SelectionType::eNormal) {
NS_ENSURE_TRUE(range.mSelection, NS_ERROR_NULL_POINTER);
} else if (!range.mSelection) {
// For non-normal selections, skip over the non-existing ones.
continue;
}
for (uint32_t j = 0; j < range.mSelection->RangeCount(); ++j) {
RefPtr<nsRange> r = range.mSelection->GetRangeAt(j);
MOZ_ASSERT(r->IsPositioned());
range.mStartContainer = r->GetStartContainer();
range.mStartOffset = r->StartOffset();
range.mEndContainer = r->GetEndContainer();
range.mEndOffset = r->EndOffset();
// If selection endpoint is between the nodes, remember it as being
// in the one that is going away instead. This simplifies later selection
// adjustment logic at end of this method.
if (range.mStartContainer) {
if (range.mStartContainer == parent &&
joinOffset < range.mStartOffset &&
range.mStartOffset <= keepOffset) {
range.mStartContainer = aNodeToJoin;
range.mStartOffset = firstNodeLength;
}
if (range.mEndContainer == parent &&
joinOffset < range.mEndOffset &&
range.mEndOffset <= keepOffset) {
range.mEndContainer = aNodeToJoin;
range.mEndOffset = firstNodeLength;
}
}
savedRanges.AppendElement(range);
}
}
// OK, ready to do join now.
// If it's a text node, just shuffle around some text.
if (IsTextNode(aNodeToKeep) && IsTextNode(aNodeToJoin)) {
nsAutoString rightText;
nsAutoString leftText;
aNodeToKeep->GetAsText()->GetData(rightText);
aNodeToJoin->GetAsText()->GetData(leftText);
leftText += rightText;
aNodeToKeep->GetAsText()->SetData(leftText, IgnoreErrors());
} else {
// Otherwise it's an interior node, so shuffle around the children.
nsCOMPtr<nsINodeList> childNodes = aNodeToJoin->ChildNodes();
MOZ_ASSERT(childNodes);
// Remember the first child in aNodeToKeep, we'll insert all the children of aNodeToJoin in front of it
// GetFirstChild returns nullptr firstNode if aNodeToKeep has no children, that's OK.
nsCOMPtr<nsIContent> firstNode = aNodeToKeep->GetFirstChild();
// Have to go through the list backwards to keep deletes from interfering with iteration.
for (uint32_t i = childNodes->Length(); i; --i) {
nsCOMPtr<nsIContent> childNode = childNodes->Item(i - 1);
if (childNode) {
// prepend children of aNodeToJoin
ErrorResult err;
aNodeToKeep->InsertBefore(*childNode, firstNode, err);
NS_ENSURE_TRUE(!err.Failed(), err.StealNSResult());
firstNode = childNode.forget();
}
}
}
// Delete the extra node.
ErrorResult err;
aParent->RemoveChild(*aNodeToJoin, err);
bool allowedTransactionsToChangeSelection =
AllowsTransactionsToChangeSelection();
RefPtr<Selection> previousSelection;
for (size_t i = 0; i < savedRanges.Length(); ++i) {
// And adjust the selection if needed.
SavedRange& range = savedRanges[i];
// If we have not seen the selection yet, clear all of its ranges.
if (range.mSelection != previousSelection) {
ErrorResult rv;
range.mSelection->RemoveAllRanges(rv);
if (NS_WARN_IF(rv.Failed())) {
return rv.StealNSResult();
}
previousSelection = range.mSelection;
}
if (allowedTransactionsToChangeSelection &&
range.mSelection->Type() == SelectionType::eNormal) {
// If the editor should adjust the selection, don't bother restoring
// the ranges for the normal selection here.
continue;
}
// Check to see if we joined nodes where selection starts.
if (range.mStartContainer == aNodeToJoin) {
range.mStartContainer = aNodeToKeep;
} else if (range.mStartContainer == aNodeToKeep) {
range.mStartOffset += firstNodeLength;
}
// Check to see if we joined nodes where selection ends.
if (range.mEndContainer == aNodeToJoin) {
range.mEndContainer = aNodeToKeep;
} else if (range.mEndContainer == aNodeToKeep) {
range.mEndOffset += firstNodeLength;
}
RefPtr<nsRange> newRange;
nsresult rv = nsRange::CreateRange(range.mStartContainer,
range.mStartOffset,
range.mEndContainer,
range.mEndOffset,
getter_AddRefs(newRange));
NS_ENSURE_SUCCESS(rv, rv);
ErrorResult err;
range.mSelection->AddRange(*newRange, err);
if (NS_WARN_IF(err.Failed())) {
return err.StealNSResult();
}
}
if (allowedTransactionsToChangeSelection) {
// Editor wants us to set selection at join point.
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
selection->Collapse(aNodeToKeep, AssertedCast<int32_t>(firstNodeLength));
}
return err.StealNSResult();
}
// static
int32_t
EditorBase::GetChildOffset(nsINode* aChild,
nsINode* aParent)
{
MOZ_ASSERT(aChild);
MOZ_ASSERT(aParent);
// nsINode::ComputeIndexOf() is expensive. So, if we can return index
// without calling it, we should do that.
// If there is no previous siblings, it means that it's the first child.
if (aParent->GetFirstChild() == aChild) {
MOZ_ASSERT(aParent->ComputeIndexOf(aChild) == 0);
return 0;
}
// If there is no next siblings, it means that it's the last child.
if (aParent->GetLastChild() == aChild) {
int32_t lastChildIndex = static_cast<int32_t>(aParent->Length() - 1);
MOZ_ASSERT(aParent->ComputeIndexOf(aChild) == lastChildIndex);
return lastChildIndex;
}
int32_t index = aParent->ComputeIndexOf(aChild);
MOZ_ASSERT(index != -1);
return index;
}
// static
nsINode*
EditorBase::GetNodeLocation(nsINode* aChild,
int32_t* aOffset)
{
MOZ_ASSERT(aChild);
MOZ_ASSERT(aOffset);
nsINode* parent = aChild->GetParentNode();
if (parent) {
*aOffset = GetChildOffset(aChild, parent);
MOZ_ASSERT(*aOffset != -1);
} else {
*aOffset = -1;
}
return parent;
}
nsIContent*
EditorBase::GetPreviousNodeInternal(nsINode& aNode,
bool aFindEditableNode,
bool aFindAnyDataNode,
bool aNoBlockCrossing)
{
if (!IsDescendantOfEditorRoot(&aNode)) {
return nullptr;
}
return FindNode(&aNode, false,
aFindEditableNode, aFindAnyDataNode, aNoBlockCrossing);
}
nsIContent*
EditorBase::GetPreviousNodeInternal(const EditorRawDOMPoint& aPoint,
bool aFindEditableNode,
bool aFindAnyDataNode,
bool aNoBlockCrossing)
{
MOZ_ASSERT(aPoint.IsSetAndValid());
NS_WARNING_ASSERTION(!aPoint.IsInDataNode() || aPoint.IsInTextNode(),
"GetPreviousNodeInternal() doesn't assume that the start point is a "
"data node except text node");
// If we are at the beginning of the node, or it is a text node, then just
// look before it.
if (aPoint.IsStartOfContainer() || aPoint.IsInTextNode()) {
if (aNoBlockCrossing && IsBlockNode(aPoint.GetContainer())) {
// If we aren't allowed to cross blocks, don't look before this block.
return nullptr;
}
return GetPreviousNodeInternal(*aPoint.GetContainer(),
aFindEditableNode, aFindAnyDataNode,
aNoBlockCrossing);
}
// else look before the child at 'aOffset'
if (aPoint.GetChild()) {
return GetPreviousNodeInternal(*aPoint.GetChild(),
aFindEditableNode, aFindAnyDataNode,
aNoBlockCrossing);
}
// unless there isn't one, in which case we are at the end of the node
// and want the deep-right child.
nsIContent* rightMostNode =
GetRightmostChild(aPoint.GetContainer(), aNoBlockCrossing);
if (!rightMostNode) {
return nullptr;
}
if ((!aFindEditableNode || IsEditable(rightMostNode)) &&
(aFindAnyDataNode || IsElementOrText(*rightMostNode))) {
return rightMostNode;
}
// restart the search from the non-editable node we just found
return GetPreviousNodeInternal(*rightMostNode,
aFindEditableNode, aFindAnyDataNode,
aNoBlockCrossing);
}
nsIContent*
EditorBase::GetNextNodeInternal(nsINode& aNode,
bool aFindEditableNode,
bool aFindAnyDataNode,
bool aNoBlockCrossing)
{
if (!IsDescendantOfEditorRoot(&aNode)) {
return nullptr;
}
return FindNode(&aNode, true,
aFindEditableNode, aFindAnyDataNode, aNoBlockCrossing);
}
nsIContent*
EditorBase::GetNextNodeInternal(const EditorRawDOMPoint& aPoint,
bool aFindEditableNode,
bool aFindAnyDataNode,
bool aNoBlockCrossing)
{
MOZ_ASSERT(aPoint.IsSetAndValid());
NS_WARNING_ASSERTION(!aPoint.IsInDataNode() || aPoint.IsInTextNode(),
"GetNextNodeInternal() doesn't assume that the start point is a "
"data node except text node");
EditorRawDOMPoint point(aPoint);
// if the container is a text node, use its location instead
if (point.IsInTextNode()) {
point.Set(point.GetContainer());
bool advanced = point.AdvanceOffset();
if (NS_WARN_IF(!advanced)) {
return nullptr;
}
}
if (point.GetChild()) {
if (aNoBlockCrossing && IsBlockNode(point.GetChild())) {
return point.GetChild();
}
nsIContent* leftMostNode =
GetLeftmostChild(point.GetChild(), aNoBlockCrossing);
if (!leftMostNode) {
return point.GetChild();
}
if (!IsDescendantOfEditorRoot(leftMostNode)) {
return nullptr;
}
if ((!aFindEditableNode || IsEditable(leftMostNode)) &&
(aFindAnyDataNode || IsElementOrText(*leftMostNode))) {
return leftMostNode;
}
// restart the search from the non-editable node we just found
return GetNextNodeInternal(*leftMostNode,
aFindEditableNode, aFindAnyDataNode,
aNoBlockCrossing);
}
// unless there isn't one, in which case we are at the end of the node
// and want the next one.
if (aNoBlockCrossing && IsBlockNode(point.GetContainer())) {
// don't cross out of parent block
return nullptr;
}
return GetNextNodeInternal(*point.GetContainer(),
aFindEditableNode, aFindAnyDataNode,
aNoBlockCrossing);
}
nsIContent*
EditorBase::FindNextLeafNode(nsINode* aCurrentNode,
bool aGoForward,
bool bNoBlockCrossing)
{
// called only by GetPriorNode so we don't need to check params.
MOZ_ASSERT(IsDescendantOfEditorRoot(aCurrentNode) &&
!IsEditorRoot(aCurrentNode),
"Bogus arguments");
nsINode* cur = aCurrentNode;
for (;;) {
// if aCurrentNode has a sibling in the right direction, return
// that sibling's closest child (or itself if it has no children)
nsIContent* sibling =
aGoForward ? cur->GetNextSibling() : cur->GetPreviousSibling();
if (sibling) {
if (bNoBlockCrossing && IsBlockNode(sibling)) {
// don't look inside prevsib, since it is a block
return sibling;
}
nsIContent *leaf =
aGoForward ? GetLeftmostChild(sibling, bNoBlockCrossing) :
GetRightmostChild(sibling, bNoBlockCrossing);
if (!leaf) {
return sibling;
}
return leaf;
}
nsINode *parent = cur->GetParentNode();
if (!parent) {
return nullptr;
}
NS_ASSERTION(IsDescendantOfEditorRoot(parent),
"We started with a proper descendant of root, and should stop "
"if we ever hit the root, so we better have a descendant of "
"root now!");
if (IsEditorRoot(parent) ||
(bNoBlockCrossing && IsBlockNode(parent))) {
return nullptr;
}
cur = parent;
}
MOZ_ASSERT_UNREACHABLE("What part of for(;;) do you not understand?");
return nullptr;
}
nsIContent*
EditorBase::FindNode(nsINode* aCurrentNode,
bool aGoForward,
bool aEditableNode,
bool aFindAnyDataNode,
bool bNoBlockCrossing)
{
if (IsEditorRoot(aCurrentNode)) {
// Don't allow traversal above the root node! This helps
// prevent us from accidentally editing browser content
// when the editor is in a text widget.
return nullptr;
}
nsCOMPtr<nsIContent> candidate =
FindNextLeafNode(aCurrentNode, aGoForward, bNoBlockCrossing);
if (!candidate) {
return nullptr;
}
if ((!aEditableNode || IsEditable(candidate)) &&
(aFindAnyDataNode || IsElementOrText(*candidate))) {
return candidate;
}
return FindNode(candidate, aGoForward,
aEditableNode, aFindAnyDataNode, bNoBlockCrossing);
}
nsIContent*
EditorBase::GetRightmostChild(nsINode* aCurrentNode,
bool bNoBlockCrossing)
{
NS_ENSURE_TRUE(aCurrentNode, nullptr);
nsIContent *cur = aCurrentNode->GetLastChild();
if (!cur) {
return nullptr;
}
for (;;) {
if (bNoBlockCrossing && IsBlockNode(cur)) {
return cur;
}
nsIContent* next = cur->GetLastChild();
if (!next) {
return cur;
}
cur = next;
}
MOZ_ASSERT_UNREACHABLE("What part of for(;;) do you not understand?");
return nullptr;
}
nsIContent*
EditorBase::GetLeftmostChild(nsINode* aCurrentNode,
bool bNoBlockCrossing)
{
NS_ENSURE_TRUE(aCurrentNode, nullptr);
nsIContent *cur = aCurrentNode->GetFirstChild();
if (!cur) {
return nullptr;
}
for (;;) {
if (bNoBlockCrossing && IsBlockNode(cur)) {
return cur;
}
nsIContent *next = cur->GetFirstChild();
if (!next) {
return cur;
}
cur = next;
}
MOZ_ASSERT_UNREACHABLE("What part of for(;;) do you not understand?");
return nullptr;
}
bool
EditorBase::IsBlockNode(nsINode* aNode)
{
// stub to be overridden in HTMLEditor.
// screwing around with the class hierarchy here in order
// to not duplicate the code in GetNextNode/GetPrevNode
// across both EditorBase/HTMLEditor.
return false;
}
bool
EditorBase::CanContain(nsINode& aParent,
nsIContent& aChild) const
{
switch (aParent.NodeType()) {
case nsINode::ELEMENT_NODE:
case nsINode::DOCUMENT_FRAGMENT_NODE:
return TagCanContain(*aParent.NodeInfo()->NameAtom(), aChild);
}
return false;
}
bool
EditorBase::CanContainTag(nsINode& aParent,
nsAtom& aChildTag) const
{
switch (aParent.NodeType()) {
case nsINode::ELEMENT_NODE:
case nsINode::DOCUMENT_FRAGMENT_NODE:
return TagCanContainTag(*aParent.NodeInfo()->NameAtom(), aChildTag);
}
return false;
}
bool
EditorBase::TagCanContain(nsAtom& aParentTag,
nsIContent& aChild) const
{
switch (aChild.NodeType()) {
case nsINode::TEXT_NODE:
case nsINode::ELEMENT_NODE:
case nsINode::DOCUMENT_FRAGMENT_NODE:
return TagCanContainTag(aParentTag, *aChild.NodeInfo()->NameAtom());
}
return false;
}
bool
EditorBase::TagCanContainTag(nsAtom& aParentTag,
nsAtom& aChildTag) const
{
return true;
}
bool
EditorBase::IsRoot(nsINode* inNode) const
{
if (NS_WARN_IF(!inNode)) {
return false;
}
nsINode* rootNode = GetRoot();
return inNode == rootNode;
}
bool
EditorBase::IsEditorRoot(nsINode* aNode) const
{
if (NS_WARN_IF(!aNode)) {
return false;
}
nsINode* rootNode = GetEditorRoot();
return aNode == rootNode;
}
bool
EditorBase::IsDescendantOfRoot(nsINode* inNode) const
{
if (NS_WARN_IF(!inNode)) {
return false;
}
nsIContent* root = GetRoot();
if (NS_WARN_IF(!root)) {
return false;
}
return nsContentUtils::ContentIsDescendantOf(inNode, root);
}
bool
EditorBase::IsDescendantOfEditorRoot(nsINode* aNode) const
{
if (NS_WARN_IF(!aNode)) {
return false;
}
nsIContent* root = GetEditorRoot();
if (NS_WARN_IF(!root)) {
return false;
}
return nsContentUtils::ContentIsDescendantOf(aNode, root);
}
bool
EditorBase::IsContainer(nsINode* aNode)
{
return aNode ? true : false;
}
uint32_t
EditorBase::CountEditableChildren(nsINode* aNode)
{
MOZ_ASSERT(aNode);
uint32_t count = 0;
for (nsIContent* child = aNode->GetFirstChild();
child;
child = child->GetNextSibling()) {
if (IsEditable(child)) {
++count;
}
}
return count;
}
NS_IMETHODIMP
EditorBase::IncrementModificationCount(int32_t inNumMods)
{
uint32_t oldModCount = mModCount;
mModCount += inNumMods;
if ((!oldModCount && mModCount) ||
(oldModCount && !mModCount)) {
NotifyDocumentListeners(eDocumentStateChanged);
}
return NS_OK;
}
NS_IMETHODIMP
EditorBase::GetModificationCount(int32_t* outModCount)
{
NS_ENSURE_ARG_POINTER(outModCount);
*outModCount = mModCount;
return NS_OK;
}
NS_IMETHODIMP
EditorBase::ResetModificationCount()
{
bool doNotify = (mModCount != 0);
mModCount = 0;
if (doNotify) {
NotifyDocumentListeners(eDocumentStateChanged);
}
return NS_OK;
}
// static
bool
EditorBase::AreNodesSameType(nsIContent& aNode1,
nsIContent& aNode2) const
{
if (aNode1.NodeInfo()->NameAtom() != aNode2.NodeInfo()->NameAtom()) {
return false;
}
if (!AsHTMLEditor() || !AsHTMLEditor()->IsCSSEnabled()) {
return true;
}
// If this is an HTMLEditor in CSS mode and they are <span> elements,
// let's check their styles.
if (!aNode1.IsHTMLElement(nsGkAtoms::span)) {
return true;
}
if (!aNode1.IsElement() || !aNode2.IsElement()) {
return false;
}
return CSSEditUtils::ElementsSameStyle(aNode1.AsElement(),
aNode2.AsElement());
}
// static
nsIContent*
EditorBase::GetNodeAtRangeOffsetPoint(const RawRangeBoundary& aPoint)
{
if (NS_WARN_IF(!aPoint.IsSet())) {
return nullptr;
}
if (aPoint.Container()->GetAsText()) {
return aPoint.Container()->AsContent();
}
return aPoint.GetChildAtOffset();
}
// static
EditorRawDOMPoint
EditorBase::GetStartPoint(Selection* aSelection)
{
MOZ_ASSERT(aSelection);
if (NS_WARN_IF(!aSelection->RangeCount())) {
return EditorRawDOMPoint();
}
const nsRange* range = aSelection->GetRangeAt(0);
if (NS_WARN_IF(!range) ||
NS_WARN_IF(!range->IsPositioned())) {
return EditorRawDOMPoint();
}
return EditorRawDOMPoint(range->StartRef());
}
// static
EditorRawDOMPoint
EditorBase::GetEndPoint(Selection* aSelection)
{
MOZ_ASSERT(aSelection);
if (NS_WARN_IF(!aSelection->RangeCount())) {
return EditorRawDOMPoint();
}
const nsRange* range = aSelection->GetRangeAt(0);
if (NS_WARN_IF(!range) ||
NS_WARN_IF(!range->IsPositioned())) {
return EditorRawDOMPoint();
}
return EditorRawDOMPoint(range->EndRef());
}
nsresult
EditorBase::GetEndChildNode(Selection* aSelection,
nsIContent** aEndNode)
{
MOZ_ASSERT(aSelection);
MOZ_ASSERT(aEndNode);
*aEndNode = nullptr;
if (NS_WARN_IF(!aSelection->RangeCount())) {
return NS_ERROR_FAILURE;
}
const nsRange* range = aSelection->GetRangeAt(0);
if (NS_WARN_IF(!range)) {
return NS_ERROR_FAILURE;
}
if (NS_WARN_IF(!range->IsPositioned())) {
return NS_ERROR_FAILURE;
}
NS_IF_ADDREF(*aEndNode = range->GetChildAtEndOffset());
return NS_OK;
}
/**
* IsPreformatted() checks the style info for the node for the preformatted
* text style.
*/
// static
bool
EditorBase::IsPreformatted(nsINode* aNode)
{
if (NS_WARN_IF(!aNode)) {
return false;
}
// Look at the node (and its parent if it's not an element), and grab its
// ComputedStyle.
Element* element = Element::FromNode(aNode);
if (!element) {
element = aNode->GetParentElement();
if (!element) {
return false;
}
}
RefPtr<ComputedStyle> elementStyle =
nsComputedDOMStyle::GetComputedStyleNoFlush(element, nullptr);
if (!elementStyle) {
// Consider nodes without a ComputedStyle to be NOT preformatted:
// For instance, this is true of JS tags inside the body (which show
// up as #text nodes but have no ComputedStyle).
return false;
}
const nsStyleText* styleText = elementStyle->StyleText();
return styleText->WhiteSpaceIsSignificant();
}
template<typename PT, typename CT>
SplitNodeResult
EditorBase::SplitNodeDeepWithTransaction(
nsIContent& aMostAncestorToSplit,
const EditorDOMPointBase<PT, CT>& aStartOfDeepestRightNode,
SplitAtEdges aSplitAtEdges)
{
MOZ_ASSERT(aStartOfDeepestRightNode.IsSetAndValid());
MOZ_ASSERT(aStartOfDeepestRightNode.GetContainer() == &aMostAncestorToSplit ||
EditorUtils::IsDescendantOf(
*aStartOfDeepestRightNode.GetContainer(),
aMostAncestorToSplit));
if (NS_WARN_IF(!aStartOfDeepestRightNode.IsSet())) {
return SplitNodeResult(NS_ERROR_INVALID_ARG);
}
nsCOMPtr<nsIContent> newLeftNodeOfMostAncestor;
EditorDOMPoint atStartOfRightNode(aStartOfDeepestRightNode);
while (true) {
// Need to insert rules code call here to do things like not split a list
// if you are after the last <li> or before the first, etc. For now we
// just have some smarts about unneccessarily splitting text nodes, which
// should be universal enough to put straight in this EditorBase routine.
if (NS_WARN_IF(!atStartOfRightNode.GetContainerAsContent())) {
return SplitNodeResult(NS_ERROR_FAILURE);
}
// If we meet an orphan node before meeting aMostAncestorToSplit, we need
// to stop splitting. This is a bug of the caller.
if (NS_WARN_IF(atStartOfRightNode.GetContainer() != &aMostAncestorToSplit &&
!atStartOfRightNode.GetContainer()->GetParent())) {
return SplitNodeResult(NS_ERROR_FAILURE);
}
nsIContent* currentRightNode = atStartOfRightNode.GetContainerAsContent();
// If the split point is middle of the node or the node is not a text node
// and we're allowed to create empty element node, split it.
if ((aSplitAtEdges == SplitAtEdges::eAllowToCreateEmptyContainer &&
!atStartOfRightNode.GetContainerAsText()) ||
(!atStartOfRightNode.IsStartOfContainer() &&
!atStartOfRightNode.IsEndOfContainer())) {
ErrorResult error;
nsCOMPtr<nsIContent> newLeftNode =
SplitNodeWithTransaction(atStartOfRightNode, error);
if (NS_WARN_IF(error.Failed())) {
return SplitNodeResult(error.StealNSResult());
}
if (currentRightNode == &aMostAncestorToSplit) {
// Actually, we split aMostAncestorToSplit.
return SplitNodeResult(newLeftNode, &aMostAncestorToSplit);
}
// Then, try to split its parent before current node.
atStartOfRightNode.Set(currentRightNode);
}
// If the split point is end of the node and it is a text node or we're not
// allowed to create empty container node, try to split its parent after it.
else if (!atStartOfRightNode.IsStartOfContainer()) {
if (currentRightNode == &aMostAncestorToSplit) {
return SplitNodeResult(&aMostAncestorToSplit, nullptr);
}
// Try to split its parent after current node.
atStartOfRightNode.Set(currentRightNode);
DebugOnly<bool> advanced = atStartOfRightNode.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after current node");
}
// If the split point is start of the node and it is a text node or we're
// not allowed to create empty container node, try to split its parent.
else {
if (currentRightNode == &aMostAncestorToSplit) {
return SplitNodeResult(nullptr, &aMostAncestorToSplit);
}
// Try to split its parent before current node.
atStartOfRightNode.Set(currentRightNode);
}
}
return SplitNodeResult(NS_ERROR_FAILURE);
}
EditorDOMPoint
EditorBase::JoinNodesDeepWithTransaction(nsIContent& aLeftNode,
nsIContent& aRightNode)
{
// While the rightmost children and their descendants of the left node match
// the leftmost children and their descendants of the right node, join them
// up.
nsCOMPtr<nsIContent> leftNodeToJoin = &aLeftNode;
nsCOMPtr<nsIContent> rightNodeToJoin = &aRightNode;
nsCOMPtr<nsINode> parentNode = aRightNode.GetParentNode();
EditorDOMPoint ret;
while (leftNodeToJoin && rightNodeToJoin && parentNode &&
AreNodesSameType(*leftNodeToJoin, *rightNodeToJoin)) {
uint32_t length = leftNodeToJoin->Length();
// Do the join
nsresult rv = JoinNodesWithTransaction(*leftNodeToJoin, *rightNodeToJoin);
if (NS_WARN_IF(NS_FAILED(rv))) {
return EditorDOMPoint();
}
ret.Set(rightNodeToJoin, length);
if (parentNode->GetAsText()) {
// We've joined all the way down to text nodes, we're done!
return ret;
}
// Get new left and right nodes, and begin anew
parentNode = rightNodeToJoin;
rightNodeToJoin = parentNode->GetChildAt_Deprecated(length);
if (rightNodeToJoin) {
leftNodeToJoin = rightNodeToJoin->GetPreviousSibling();
} else {
leftNodeToJoin = nullptr;
}
// Skip over non-editable nodes
while (leftNodeToJoin && !IsEditable(leftNodeToJoin)) {
leftNodeToJoin = leftNodeToJoin->GetPreviousSibling();
}
if (!leftNodeToJoin) {
return ret;
}
while (rightNodeToJoin && !IsEditable(rightNodeToJoin)) {
rightNodeToJoin = rightNodeToJoin->GetNextSibling();
}
if (!rightNodeToJoin) {
return ret;
}
}
if (NS_WARN_IF(!ret.IsSet())) {
return EditorDOMPoint();
}
return ret;
}
void
EditorBase::BeginUpdateViewBatch()
{
MOZ_ASSERT(mUpdateCount >= 0, "bad state");
if (!mUpdateCount) {
// Turn off selection updates and notifications.
RefPtr<Selection> selection = GetSelection();
if (selection) {
selection->StartBatchChanges();
}
}
mUpdateCount++;
}
void
EditorBase::EndUpdateViewBatch()
{
MOZ_ASSERT(mUpdateCount > 0, "bad state");
if (mUpdateCount <= 0) {
mUpdateCount = 0;
return;
}
if (--mUpdateCount) {
return;
}
// Turn selection updating and notifications back on.
RefPtr<Selection> selection = GetSelection();
if (selection) {
selection->EndBatchChanges();
}
HTMLEditor* htmlEditor = AsHTMLEditor();
if (!htmlEditor) {
return;
}
// We may need to show resizing handles or update existing ones after
// all transactions are done. This way of doing is preferred to DOM
// mutation events listeners because all the changes the user can apply
// to a document may result in multiple events, some of them quite hard
// to listen too (in particular when an ancestor of the selection is
// changed but the selection itself is not changed).
if (NS_WARN_IF(!selection)) {
return;
}
DebugOnly<nsresult> rv = htmlEditor->RefereshEditingUI(*selection);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "RefereshEditingUI() failed");
}
TextComposition*
EditorBase::GetComposition() const
{
return mComposition;
}
bool
EditorBase::IsIMEComposing() const
{
return mComposition && mComposition->IsComposing();
}
bool
EditorBase::ShouldHandleIMEComposition() const
{
// When the editor is being reframed, the old value may be restored with
// InsertText(). In this time, the text should be inserted as not a part
// of the composition.
return mComposition && mDidPostCreate;
}
void
EditorBase::DoAfterDoTransaction(nsITransaction* aTxn)
{
bool isTransientTransaction;
MOZ_ALWAYS_SUCCEEDS(aTxn->GetIsTransient(&isTransientTransaction));
if (!isTransientTransaction) {
// we need to deal here with the case where the user saved after some
// edits, then undid one or more times. Then, the undo count is -ve,
// but we can't let a do take it back to zero. So we flip it up to
// a +ve number.
int32_t modCount;
GetModificationCount(&modCount);
if (modCount < 0) {
modCount = -modCount;
}
// don't count transient transactions
MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1));
}
}
void
EditorBase::DoAfterUndoTransaction()
{
// all undoable transactions are non-transient
MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(-1));
}
void
EditorBase::DoAfterRedoTransaction()
{
// all redoable transactions are non-transient
MOZ_ALWAYS_SUCCEEDS(IncrementModificationCount(1));
}
already_AddRefed<EditAggregateTransaction>
EditorBase::CreateTxnForDeleteSelection(EDirection aAction,
nsINode** aRemovingNode,
int32_t* aOffset,
int32_t* aLength)
{
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return nullptr;
}
// Check whether the selection is collapsed and we should do nothing:
if (NS_WARN_IF(selection->IsCollapsed() && aAction == eNone)) {
return nullptr;
}
// allocate the out-param transaction
RefPtr<EditAggregateTransaction> aggregateTransaction =
EditAggregateTransaction::Create();
for (uint32_t rangeIdx = 0; rangeIdx < selection->RangeCount(); ++rangeIdx) {
RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx);
if (NS_WARN_IF(!range)) {
return nullptr;
}
// Same with range as with selection; if it is collapsed and action
// is eNone, do nothing.
if (!range->Collapsed()) {
RefPtr<DeleteRangeTransaction> deleteRangeTransaction =
DeleteRangeTransaction::Create(*this, *range);
// XXX Oh, not checking if deleteRangeTransaction can modify the range...
aggregateTransaction->AppendChild(deleteRangeTransaction);
} else if (aAction != eNone) {
// we have an insertion point. delete the thing in front of it or
// behind it, depending on aAction
// XXX Odd, when there are two or more ranges, this returns the last
// range information with aRemovingNode, aOffset and aLength.
RefPtr<EditTransactionBase> deleteRangeTransaction =
CreateTxnForDeleteRange(range, aAction,
aRemovingNode, aOffset, aLength);
// XXX When there are two or more ranges and at least one of them is
// not editable, deleteRangeTransaction may be nullptr.
// In such case, should we stop removing other ranges too?
if (NS_WARN_IF(!deleteRangeTransaction)) {
return nullptr;
}
aggregateTransaction->AppendChild(deleteRangeTransaction);
}
}
return aggregateTransaction.forget();
}
//XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior
//are not implemented
already_AddRefed<EditTransactionBase>
EditorBase::CreateTxnForDeleteRange(nsRange* aRangeToDelete,
EDirection aAction,
nsINode** aRemovingNode,
int32_t* aOffset,
int32_t* aLength)
{
MOZ_ASSERT(aAction != eNone);
// get the node and offset of the insertion point
nsCOMPtr<nsINode> node = aRangeToDelete->GetStartContainer();
if (NS_WARN_IF(!node)) {
return nullptr;
}
nsIContent* child = aRangeToDelete->GetChildAtStartOffset();
int32_t offset = aRangeToDelete->StartOffset();
// determine if the insertion point is at the beginning, middle, or end of
// the node
uint32_t count = node->Length();
bool isFirst = !offset;
bool isLast = (count == (uint32_t)offset);
// XXX: if isFirst && isLast, then we'll need to delete the node
// as well as the 1 child
// build a transaction for deleting the appropriate data
// XXX: this has to come from rule section
if (aAction == ePrevious && isFirst) {
// we're backspacing from the beginning of the node. Delete the first
// thing to our left
nsCOMPtr<nsIContent> priorNode = GetPreviousEditableNode(*node);
if (NS_WARN_IF(!priorNode)) {
return nullptr;
}
// there is a priorNode, so delete its last child (if chardata, delete the
// last char). if it has no children, delete it
if (RefPtr<CharacterData> priorNodeAsCharData =
CharacterData::FromNode(priorNode)) {
uint32_t length = priorNode->Length();
// Bail out for empty chardata XXX: Do we want to do something else?
if (NS_WARN_IF(!length)) {
return nullptr;
}
RefPtr<DeleteTextTransaction> deleteTextTransaction =
DeleteTextTransaction::MaybeCreateForPreviousCharacter(
*this, *priorNodeAsCharData, length);
if (NS_WARN_IF(!deleteTextTransaction)) {
return nullptr;
}
*aOffset = deleteTextTransaction->Offset();
*aLength = deleteTextTransaction->LengthToDelete();
priorNode.forget(aRemovingNode);
return deleteTextTransaction.forget();
}
// priorNode is not chardata, so tell its parent to delete it
RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
DeleteNodeTransaction::MaybeCreate(*this, *priorNode);
if (NS_WARN_IF(!deleteNodeTransaction)) {
return nullptr;
}
priorNode.forget(aRemovingNode);
return deleteNodeTransaction.forget();
}
if (aAction == eNext && isLast) {
// we're deleting from the end of the node. Delete the first thing to our
// right
nsCOMPtr<nsIContent> nextNode = GetNextEditableNode(*node);
if (NS_WARN_IF(!nextNode)) {
return nullptr;
}
// there is a nextNode, so delete its first child (if chardata, delete the
// first char). if it has no children, delete it
if (RefPtr<CharacterData> nextNodeAsCharData =
CharacterData::FromNode(nextNode)) {
uint32_t length = nextNode->Length();
// Bail out for empty chardata XXX: Do we want to do something else?
if (NS_WARN_IF(!length)) {
return nullptr;
}
RefPtr<DeleteTextTransaction> deleteTextTransaction =
DeleteTextTransaction::MaybeCreateForNextCharacter(
*this, *nextNodeAsCharData, 0);
if (NS_WARN_IF(!deleteTextTransaction)) {
return nullptr;
}
*aOffset = deleteTextTransaction->Offset();
*aLength = deleteTextTransaction->LengthToDelete();
nextNode.forget(aRemovingNode);
return deleteTextTransaction.forget();
}
// nextNode is not chardata, so tell its parent to delete it
RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
DeleteNodeTransaction::MaybeCreate(*this, *nextNode);
if (NS_WARN_IF(!deleteNodeTransaction)) {
return nullptr;
}
nextNode.forget(aRemovingNode);
return deleteNodeTransaction.forget();
}
if (RefPtr<CharacterData> nodeAsCharData = CharacterData::FromNode(node)) {
if (NS_WARN_IF(aAction != ePrevious && aAction != eNext)) {
return nullptr;
}
// We have chardata, so delete a char at the proper offset
RefPtr<DeleteTextTransaction> deleteTextTransaction =
aAction == ePrevious ?
DeleteTextTransaction::MaybeCreateForPreviousCharacter(
*this, *nodeAsCharData, offset) :
DeleteTextTransaction::MaybeCreateForNextCharacter(
*this, *nodeAsCharData, offset);
if (NS_WARN_IF(!deleteTextTransaction)) {
return nullptr;
}
*aOffset = deleteTextTransaction->Offset();
*aLength = deleteTextTransaction->LengthToDelete();
node.forget(aRemovingNode);
return deleteTextTransaction.forget();
}
// we're either deleting a node or chardata, need to dig into the next/prev
// node to find out
nsCOMPtr<nsINode> selectedNode;
if (aAction == ePrevious) {
selectedNode =
GetPreviousEditableNode(EditorRawDOMPoint(node, child, offset));
} else if (aAction == eNext) {
selectedNode = GetNextEditableNode(EditorRawDOMPoint(node, child, offset));
}
while (selectedNode &&
selectedNode->IsCharacterData() &&
!selectedNode->Length()) {
// Can't delete an empty chardata node (bug 762183)
if (aAction == ePrevious) {
selectedNode = GetPreviousEditableNode(*selectedNode);
} else if (aAction == eNext) {
selectedNode = GetNextEditableNode(*selectedNode);
}
}
if (NS_WARN_IF(!selectedNode)) {
return nullptr;
}
if (RefPtr<CharacterData> selectedNodeAsCharData =
CharacterData::FromNode(selectedNode)) {
if (NS_WARN_IF(aAction != ePrevious && aAction != eNext)) {
return nullptr;
}
// we are deleting from a chardata node, so do a character deletion
uint32_t position = 0;
if (aAction == ePrevious) {
position = selectedNode->Length();
}
RefPtr<DeleteTextTransaction> deleteTextTransaction =
aAction == ePrevious ?
DeleteTextTransaction::MaybeCreateForPreviousCharacter(
*this, *selectedNodeAsCharData, position) :
DeleteTextTransaction::MaybeCreateForNextCharacter(
*this, *selectedNodeAsCharData, position);
if (NS_WARN_IF(!deleteTextTransaction)) {
return nullptr;
}
*aOffset = deleteTextTransaction->Offset();
*aLength = deleteTextTransaction->LengthToDelete();
selectedNode.forget(aRemovingNode);
return deleteTextTransaction.forget();
}
RefPtr<DeleteNodeTransaction> deleteNodeTransaction =
DeleteNodeTransaction::MaybeCreate(*this, *selectedNode);
if (NS_WARN_IF(!deleteNodeTransaction)) {
return nullptr;
}
selectedNode.forget(aRemovingNode);
return deleteNodeTransaction.forget();
}
nsresult
EditorBase::CreateRange(nsINode* aStartContainer,
int32_t aStartOffset,
nsINode* aEndContainer,
int32_t aEndOffset,
nsRange** aRange)
{
return nsRange::CreateRange(aStartContainer, aStartOffset,
aEndContainer, aEndOffset, aRange);
}
nsresult
EditorBase::AppendNodeToSelectionAsRange(nsINode* aNode)
{
NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
nsCOMPtr<nsINode> parentNode = aNode->GetParentNode();
NS_ENSURE_TRUE(parentNode, NS_ERROR_NULL_POINTER);
int32_t offset = GetChildOffset(aNode, parentNode);
RefPtr<nsRange> range;
nsresult rv = CreateRange(parentNode, offset, parentNode, offset + 1,
getter_AddRefs(range));
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_TRUE(range, NS_ERROR_NULL_POINTER);
ErrorResult err;
selection->AddRange(*range, err);
return err.StealNSResult();
}
nsresult
EditorBase::ClearSelection()
{
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_FAILURE);
ErrorResult rv;
selection->RemoveAllRanges(rv);
return rv.StealNSResult();
}
already_AddRefed<Element>
EditorBase::CreateHTMLContent(const nsAtom* aTag)
{
MOZ_ASSERT(aTag);
nsCOMPtr<nsIDocument> doc = GetDocument();
if (!doc) {
return nullptr;
}
// XXX Wallpaper over editor bug (editor tries to create elements with an
// empty nodename).
if (aTag == nsGkAtoms::_empty) {
NS_ERROR("Don't pass an empty tag to EditorBase::CreateHTMLContent, "
"check caller.");
return nullptr;
}
return doc->CreateElem(nsDependentAtomString(aTag), nullptr,
kNameSpaceID_XHTML);
}
// static
already_AddRefed<nsTextNode>
EditorBase::CreateTextNode(nsIDocument& aDocument,
const nsAString& aData)
{
RefPtr<nsTextNode> text = aDocument.CreateEmptyTextNode();
text->MarkAsMaybeModifiedFrequently();
// Don't notify; this node is still being created.
text->SetText(aData, false);
return text.forget();
}
NS_IMETHODIMP
EditorBase::SetAttributeOrEquivalent(Element* aElement,
const nsAString& aAttribute,
const nsAString& aValue,
bool aSuppressTransaction)
{
if (NS_WARN_IF(!aElement)) {
return NS_ERROR_NULL_POINTER;
}
RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
return SetAttributeOrEquivalent(aElement, attribute, aValue,
aSuppressTransaction);
}
NS_IMETHODIMP
EditorBase::RemoveAttributeOrEquivalent(Element* aElement,
const nsAString& aAttribute,
bool aSuppressTransaction)
{
if (NS_WARN_IF(!aElement)) {
return NS_ERROR_NULL_POINTER;
}
RefPtr<nsAtom> attribute = NS_Atomize(aAttribute);
return RemoveAttributeOrEquivalent(aElement, attribute, aSuppressTransaction);
}
nsresult
EditorBase::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent)
{
// NOTE: When you change this method, you should also change:
// * editor/libeditor/tests/test_texteditor_keyevent_handling.html
// * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
//
// And also when you add new key handling, you need to change the subclass's
// HandleKeyPressEvent()'s switch statement.
if (NS_WARN_IF(!aKeyboardEvent)) {
return NS_ERROR_UNEXPECTED;
}
MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress,
"HandleKeyPressEvent gets non-keypress event");
// if we are readonly or disabled, then do nothing.
if (IsReadonly() || IsDisabled()) {
// consume backspace for disabled and readonly textfields, to prevent
// back in history, which could be confusing to users
if (aKeyboardEvent->mKeyCode == NS_VK_BACK) {
aKeyboardEvent->PreventDefault();
}
return NS_OK;
}
switch (aKeyboardEvent->mKeyCode) {
case NS_VK_META:
case NS_VK_WIN:
case NS_VK_SHIFT:
case NS_VK_CONTROL:
case NS_VK_ALT:
aKeyboardEvent->PreventDefault(); // consumed
return NS_OK;
}
return NS_OK;
}
nsresult
EditorBase::HandleInlineSpellCheck(EditSubAction aEditSubAction,
Selection& aSelection,
nsINode* previousSelectedNode,
uint32_t previousSelectedOffset,
nsINode* aStartContainer,
uint32_t aStartOffset,
nsINode* aEndContainer,
uint32_t aEndOffset)
{
if (!mInlineSpellChecker) {
return NS_OK;
}
return mInlineSpellChecker->SpellCheckAfterEditorChange(
aEditSubAction, aSelection,
previousSelectedNode, previousSelectedOffset,
aStartContainer, aStartOffset, aEndContainer,
aEndOffset);
}
Element*
EditorBase::FindSelectionRoot(nsINode* aNode) const
{
return GetRoot();
}
void
EditorBase::InitializeSelectionAncestorLimit(Selection& aSelection,
nsIContent& aAncestorLimit)
{
aSelection.SetAncestorLimiter(&aAncestorLimit);
}
nsresult
EditorBase::InitializeSelection(EventTarget* aFocusEventTarget)
{
nsCOMPtr<nsINode> targetNode = do_QueryInterface(aFocusEventTarget);
NS_ENSURE_TRUE(targetNode, NS_ERROR_INVALID_ARG);
nsCOMPtr<nsIContent> selectionRootContent = FindSelectionRoot(targetNode);
if (!selectionRootContent) {
return NS_OK;
}
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_STATE(selection);
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED);
nsCOMPtr<nsISelectionController> selectionController =
GetSelectionController();
if (NS_WARN_IF(!selectionController)) {
return NS_ERROR_FAILURE;
}
// Init the caret
RefPtr<nsCaret> caret = presShell->GetCaret();
NS_ENSURE_TRUE(caret, NS_ERROR_UNEXPECTED);
caret->SetIgnoreUserModify(false);
caret->SetSelection(selection);
selectionController->SetCaretReadOnly(IsReadonly());
selectionController->SetCaretEnabled(true);
// Init selection
selectionController->SetDisplaySelection(
nsISelectionController::SELECTION_ON);
selectionController->SetSelectionFlags(
nsISelectionDisplay::DISPLAY_ALL);
selectionController->RepaintSelection(
nsISelectionController::SELECTION_NORMAL);
// If the computed selection root isn't root content, we should set it
// as selection ancestor limit. However, if that is root element, it means
// there is not limitation of the selection, then, we must set nullptr.
// NOTE: If we set a root element to the ancestor limit, some selection
// methods don't work fine.
if (selectionRootContent->GetParent()) {
InitializeSelectionAncestorLimit(*selection, *selectionRootContent);
} else {
selection->SetAncestorLimiter(nullptr);
}
// If there is composition when this is called, we may need to restore IME
// selection because if the editor is reframed, this already forgot IME
// selection and the transaction.
if (mComposition && mComposition->IsMovingToNewTextNode()) {
// We need to look for the new text node from current selection.
// XXX If selection is changed during reframe, this doesn't work well!
nsRange* firstRange = selection->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint atStartOfFirstRange(firstRange->StartRef());
EditorRawDOMPoint betterInsertionPoint =
FindBetterInsertionPoint(atStartOfFirstRange);
Text* textNode = betterInsertionPoint.GetContainerAsText();
MOZ_ASSERT(textNode,
"There must be text node if composition string is not empty");
if (textNode) {
MOZ_ASSERT(textNode->Length() >= mComposition->XPEndOffsetInTextNode(),
"The text node must be different from the old text node");
CompositionTransaction::SetIMESelection(
*this, textNode,
mComposition->XPOffsetInTextNode(),
mComposition->XPLengthInTextNode(),
mComposition->GetRanges());
}
}
return NS_OK;
}
class RepaintSelectionRunner final : public Runnable {
public:
explicit RepaintSelectionRunner(nsISelectionController* aSelectionController)
: Runnable("RepaintSelectionRunner")
, mSelectionController(aSelectionController)
{
}
NS_IMETHOD Run() override
{
mSelectionController->RepaintSelection(
nsISelectionController::SELECTION_NORMAL);
return NS_OK;
}
private:
nsCOMPtr<nsISelectionController> mSelectionController;
};
nsresult
EditorBase::FinalizeSelection()
{
nsCOMPtr<nsISelectionController> selectionController =
GetSelectionController();
if (NS_WARN_IF(!selectionController)) {
return NS_ERROR_FAILURE;
}
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_STATE(selection);
selection->SetAncestorLimiter(nullptr);
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_INITIALIZED);
selectionController->SetCaretEnabled(false);
nsFocusManager* fm = nsFocusManager::GetFocusManager();
NS_ENSURE_TRUE(fm, NS_ERROR_NOT_INITIALIZED);
fm->UpdateCaretForCaretBrowsingMode();
if (!HasIndependentSelection()) {
// If this editor doesn't have an independent selection, i.e., it must
// mean that it is an HTML editor, the selection controller is shared with
// presShell. So, even this editor loses focus, other part of the document
// may still have focus.
nsCOMPtr<nsIDocument> doc = GetDocument();
ErrorResult ret;
if (!doc || !doc->HasFocus(ret)) {
// If the document already lost focus, mark the selection as disabled.
selectionController->SetDisplaySelection(
nsISelectionController::SELECTION_DISABLED);
} else {
// Otherwise, mark selection as normal because outside of a
// contenteditable element should be selected with normal selection
// color after here.
selectionController->SetDisplaySelection(
nsISelectionController::SELECTION_ON);
}
} else if (IsFormWidget() || IsPasswordEditor() ||
IsReadonly() || IsDisabled() || IsInputFiltered()) {
// In <input> or <textarea>, the independent selection should be hidden
// while this editor doesn't have focus.
selectionController->SetDisplaySelection(
nsISelectionController::SELECTION_HIDDEN);
} else {
// Otherwise, although we're not sure how this case happens, the
// independent selection should be marked as disabled.
selectionController->SetDisplaySelection(
nsISelectionController::SELECTION_DISABLED);
}
// FinalizeSelection might be called from ContentRemoved even if selection
// isn't updated. So we need to call RepaintSelection after updated it.
nsContentUtils::AddScriptRunner(
new RepaintSelectionRunner(selectionController));
return NS_OK;
}
void
EditorBase::ReinitializeSelection(Element& aElement)
{
if (NS_WARN_IF(Destroyed())) {
return;
}
OnFocus(&aElement);
// If previous focused editor turn on spellcheck and this editor doesn't
// turn on it, spellcheck state is mismatched. So we need to re-sync it.
SyncRealTimeSpell();
nsPresContext* context = GetPresContext();
if (NS_WARN_IF(!context)) {
return;
}
nsCOMPtr<nsIContent> focusedContent = GetFocusedContentForIME();
IMEStateManager::OnFocusInEditor(context, focusedContent, *this);
}
Element*
EditorBase::GetEditorRoot() const
{
return GetRoot();
}
Element*
EditorBase::GetExposedRoot() const
{
Element* rootElement = GetRoot();
// For plaintext editors, we need to ask the input/textarea element directly.
if (rootElement && rootElement->IsRootOfNativeAnonymousSubtree()) {
rootElement = rootElement->GetParent()->AsElement();
}
return rootElement;
}
nsresult
EditorBase::DetermineCurrentDirection()
{
// Get the current root direction from its frame
nsIContent* rootElement = GetExposedRoot();
NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE);
// If we don't have an explicit direction, determine our direction
// from the content's direction
if (!IsRightToLeft() && !IsLeftToRight()) {
nsIFrame* frame = rootElement->GetPrimaryFrame();
NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
// Set the flag here, to enable us to use the same code path below.
// It will be flipped before returning from the function.
if (frame->StyleVisibility()->mDirection == NS_STYLE_DIRECTION_RTL) {
mFlags |= nsIPlaintextEditor::eEditorRightToLeft;
} else {
mFlags |= nsIPlaintextEditor::eEditorLeftToRight;
}
}
return NS_OK;
}
nsresult
EditorBase::ToggleTextDirection()
{
// XXX Oddly, Chrome does not dispatch beforeinput event in this case but
// dispatches input event.
nsresult rv = DetermineCurrentDirection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (IsRightToLeft()) {
nsresult rv = SetTextDirectionTo(TextDirection::eLTR);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
} else if (IsLeftToRight()) {
nsresult rv = SetTextDirectionTo(TextDirection::eRTL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
// XXX When we don't change the text direction, do we really need to
// dispatch input event?
FireInputEvent();
return NS_OK;
}
void
EditorBase::SwitchTextDirectionTo(TextDirection aTextDirection)
{
// XXX Oddly, Chrome does not dispatch beforeinput event in this case but
// dispatches input event.
nsresult rv = DetermineCurrentDirection();
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
if (aTextDirection == TextDirection::eLTR && IsRightToLeft()) {
if (NS_WARN_IF(NS_FAILED(SetTextDirectionTo(aTextDirection)))) {
return;
}
} else if (aTextDirection == TextDirection::eRTL && IsLeftToRight()) {
if (NS_WARN_IF(NS_FAILED(SetTextDirectionTo(aTextDirection)))) {
return;
}
}
// XXX When we don't change the text direction, do we really need to
// dispatch input event?
FireInputEvent();
}
nsresult
EditorBase::SetTextDirectionTo(TextDirection aTextDirection)
{
Element* rootElement = GetExposedRoot();
if (aTextDirection == TextDirection::eLTR) {
NS_ASSERTION(!IsLeftToRight(), "Unexpected mutually exclusive flag");
mFlags &= ~nsIPlaintextEditor::eEditorRightToLeft;
mFlags |= nsIPlaintextEditor::eEditorLeftToRight;
nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir,
NS_LITERAL_STRING("ltr"), true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (aTextDirection == TextDirection::eRTL) {
NS_ASSERTION(!IsRightToLeft(), "Unexpected mutually exclusive flag");
mFlags |= nsIPlaintextEditor::eEditorRightToLeft;
mFlags &= ~nsIPlaintextEditor::eEditorLeftToRight;
nsresult rv = rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::dir,
NS_LITERAL_STRING("rtl"), true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
return NS_OK;
}
bool
EditorBase::IsModifiableNode(const nsINode& aNode) const
{
return !AsHTMLEditor() || aNode.IsEditable();
}
nsIContent*
EditorBase::GetFocusedContent()
{
EventTarget* piTarget = GetDOMEventTarget();
if (!piTarget) {
return nullptr;
}
nsFocusManager* fm = nsFocusManager::GetFocusManager();
NS_ENSURE_TRUE(fm, nullptr);
nsIContent* content = fm->GetFocusedElement();
MOZ_ASSERT((content == piTarget) == SameCOMIdentity(content, piTarget));
return (content == piTarget) ? content : nullptr;
}
already_AddRefed<nsIContent>
EditorBase::GetFocusedContentForIME()
{
nsCOMPtr<nsIContent> content = GetFocusedContent();
return content.forget();
}
bool
EditorBase::IsActiveInDOMWindow()
{
EventTarget* piTarget = GetDOMEventTarget();
if (!piTarget) {
return false;
}
nsFocusManager* fm = nsFocusManager::GetFocusManager();
NS_ENSURE_TRUE(fm, false);
nsCOMPtr<nsIDocument> document = GetDocument();
if (NS_WARN_IF(!document)) {
return false;
}
nsPIDOMWindowOuter* ourWindow = document->GetWindow();
nsCOMPtr<nsPIDOMWindowOuter> win;
nsIContent* content =
nsFocusManager::GetFocusedDescendant(ourWindow,
nsFocusManager::eOnlyCurrentWindow,
getter_AddRefs(win));
return SameCOMIdentity(content, piTarget);
}
bool
EditorBase::IsAcceptableInputEvent(WidgetGUIEvent* aGUIEvent)
{
// If the event is trusted, the event should always cause input.
if (NS_WARN_IF(!aGUIEvent)) {
return false;
}
// If this is dispatched by using cordinates but this editor doesn't have
// focus, we shouldn't handle it.
if (aGUIEvent->IsUsingCoordinates()) {
nsIContent* focusedContent = GetFocusedContent();
if (!focusedContent) {
return false;
}
}
// If a composition event isn't dispatched via widget, we need to ignore them
// since they cannot be managed by TextComposition. E.g., the event was
// created by chrome JS.
// Note that if we allow to handle such events, editor may be confused by
// strange event order.
bool needsWidget = false;
switch (aGUIEvent->mMessage) {
case eUnidentifiedEvent:
// If events are not created with proper event interface, their message
// are initialized with eUnidentifiedEvent. Let's ignore such event.
return false;
case eCompositionStart:
case eCompositionEnd:
case eCompositionUpdate:
case eCompositionChange:
case eCompositionCommitAsIs:
// Don't allow composition events whose internal event are not
// WidgetCompositionEvent.
if (!aGUIEvent->AsCompositionEvent()) {
return false;
}
needsWidget = true;
break;
default:
break;
}
if (needsWidget && !aGUIEvent->mWidget) {
return false;
}
// Accept all trusted events.
if (aGUIEvent->IsTrusted()) {
return true;
}
// Ignore untrusted mouse event.
// XXX Why are we handling other untrusted input events?
if (aGUIEvent->AsMouseEventBase()) {
return false;
}
// Otherwise, we shouldn't handle any input events when we're not an active
// element of the DOM window.
return IsActiveInDOMWindow();
}
void
EditorBase::OnFocus(EventTarget* aFocusEventTarget)
{
InitializeSelection(aFocusEventTarget);
mSpellCheckerDictionaryUpdated = false;
if (mInlineSpellChecker && CanEnableSpellCheck()) {
mInlineSpellChecker->UpdateCurrentDictionary();
mSpellCheckerDictionaryUpdated = true;
}
}
int32_t
EditorBase::GetIMESelectionStartOffsetIn(nsINode* aTextNode)
{
MOZ_ASSERT(aTextNode, "aTextNode must not be nullptr");
nsISelectionController* selectionController = GetSelectionController();
if (NS_WARN_IF(!selectionController)) {
return -1;
}
uint32_t minOffset = UINT32_MAX;
static const SelectionType kIMESelectionTypes[] = {
SelectionType::eIMERawClause,
SelectionType::eIMESelectedRawClause,
SelectionType::eIMEConvertedClause,
SelectionType::eIMESelectedClause
};
for (auto selectionType : kIMESelectionTypes) {
RefPtr<Selection> selection = GetSelection(selectionType);
if (!selection) {
continue;
}
for (uint32_t i = 0; i < selection->RangeCount(); i++) {
RefPtr<nsRange> range = selection->GetRangeAt(i);
if (NS_WARN_IF(!range)) {
continue;
}
if (NS_WARN_IF(range->GetStartContainer() != aTextNode)) {
// ignore the start offset...
} else {
minOffset = std::min(minOffset, range->StartOffset());
}
if (NS_WARN_IF(range->GetEndContainer() != aTextNode)) {
// ignore the end offset...
} else {
minOffset = std::min(minOffset, range->EndOffset());
}
}
}
return minOffset < INT32_MAX ? minOffset : -1;
}
void
EditorBase::HideCaret(bool aHide)
{
if (mHidingCaret == aHide) {
return;
}
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
NS_ENSURE_TRUE_VOID(presShell);
RefPtr<nsCaret> caret = presShell->GetCaret();
NS_ENSURE_TRUE_VOID(caret);
mHidingCaret = aHide;
if (aHide) {
caret->AddForceHide();
} else {
caret->RemoveForceHide();
}
}
} // namespace mozilla