Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "EditorBase.h"
#include <stdio.h> // for nullptr, stdout
#include <string.h> // for strcmp
#include "AutoRangeArray.h" // for AutoRangeArray
#include "ChangeAttributeTransaction.h"
#include "CompositionTransaction.h"
#include "DeleteContentTransactionBase.h"
#include "DeleteMultipleRangesTransaction.h"
#include "DeleteNodeTransaction.h"
#include "DeleteRangeTransaction.h"
#include "DeleteTextTransaction.h"
#include "EditAction.h" // for EditSubAction
#include "EditorDOMPoint.h" // for EditorDOMPoint
#include "EditorUtils.h" // for various helper classes.
#include "EditTransactionBase.h" // for EditTransactionBase
#include "EditorEventListener.h" // for EditorEventListener
#include "HTMLEditor.h" // for HTMLEditor
#include "HTMLEditorInlines.h"
#include "HTMLEditUtils.h" // for HTMLEditUtils
#include "InsertNodeTransaction.h" // for InsertNodeTransaction
#include "InsertTextTransaction.h" // for InsertTextTransaction
#include "JoinNodesTransaction.h" // for JoinNodesTransaction
#include "PlaceholderTransaction.h" // for PlaceholderTransaction
#include "SplitNodeTransaction.h" // for SplitNodeTransaction
#include "TextEditor.h" // for TextEditor
#include "ErrorList.h"
#include "gfxFontUtils.h" // for gfxFontUtils
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/BasePrincipal.h" // for BasePrincipal
#include "mozilla/CheckedInt.h" // for CheckedInt
#include "mozilla/ComposerCommandsUpdater.h" // for ComposerCommandsUpdater
#include "mozilla/ContentEvents.h" // for InternalClipboardEvent
#include "mozilla/DebugOnly.h" // for DebugOnly
#include "mozilla/EditorSpellCheck.h" // for EditorSpellCheck
#include "mozilla/Encoding.h" // for Encoding (used in Document::GetDocumentCharacterSet)
#include "mozilla/EventDispatcher.h" // for EventChainPreVisitor, etc.
#include "mozilla/FlushType.h" // for FlushType::Frames
#include "mozilla/IMEContentObserver.h" // for IMEContentObserver
#include "mozilla/IMEStateManager.h" // for IMEStateManager
#include "mozilla/InputEventOptions.h" // for InputEventOptions
#include "mozilla/IntegerRange.h" // for IntegerRange
#include "mozilla/InternalMutationEvent.h" // for NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED
#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/PresShell.h" // for PresShell
#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary
#include "mozilla/Services.h" // for GetObserverService
#include "mozilla/StaticPrefs_bidi.h" // for StaticPrefs::bidi_*
#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_*
#include "mozilla/StaticPrefs_editor.h" // for StaticPrefs::editor_*
#include "mozilla/StaticPrefs_layout.h" // for StaticPrefs::layout_*
#include "mozilla/TextComposition.h" // for TextComposition
#include "mozilla/TextControlElement.h" // for TextControlElement
#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/AbstractRange.h" // for AbstractRange
#include "mozilla/dom/Attr.h" // for Attr
#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext
#include "mozilla/dom/CharacterData.h" // for CharacterData
#include "mozilla/dom/DataTransfer.h" // for DataTransfer
#include "mozilla/dom/Document.h" // for Document
#include "mozilla/dom/DocumentInlines.h" // for GetObservingPresShell
#include "mozilla/dom/DragEvent.h" // for DragEvent
#include "mozilla/dom/Element.h" // for Element, nsINode::AsElement
#include "mozilla/dom/EventTarget.h" // for EventTarget
#include "mozilla/dom/HTMLBodyElement.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Selection.h" // for Selection, etc.
#include "mozilla/dom/StaticRange.h" // for StaticRange
#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 "nsContentUtils.h" // for nsContentUtils
#include "nsCopySupport.h" // for nsCopySupport
#include "nsDOMString.h" // for DOMStringIsNull
#include "nsDebug.h" // for NS_WARNING, 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 "nsIClipboard.h" // for nsIClipboard
#include "nsIContent.h" // for nsIContent
#include "nsIContentInlines.h" // for nsINode::IsInDesignMode()
#include "nsIDocumentEncoder.h" // for nsIDocumentEncoder
#include "nsIDocumentStateListener.h" // for nsIDocumentStateListener
#include "nsIDocShell.h" // for nsIDocShell
#include "nsIEditActionListener.h" // for nsIEditActionListener
#include "nsIFrame.h" // for nsIFrame
#include "nsIInlineSpellChecker.h" // for nsIInlineSpellChecker, etc.
#include "nsNameSpaceManager.h" // for kNameSpaceID_None, etc.
#include "nsINode.h" // for nsINode, etc.
#include "nsISelectionController.h" // for nsISelectionController, etc.
#include "nsISelectionDisplay.h" // for nsISelectionDisplay, etc.
#include "nsISupports.h" // for nsISupports
#include "nsISupportsUtils.h" // for NS_ADDREF, NS_IF_ADDREF
#include "nsITransferable.h" // for nsITransferable
#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 StyleDirection::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;
using EmptyCheckOption = HTMLEditUtils::EmptyCheckOption;
using LeafNodeType = HTMLEditUtils::LeafNodeType;
using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
using WalkTreeOption = HTMLEditUtils::WalkTreeOption;
/*****************************************************************************
* mozilla::EditorBase
*****************************************************************************/
template EditorDOMPoint EditorBase::GetFirstIMESelectionStartPoint() const;
template EditorRawDOMPoint EditorBase::GetFirstIMESelectionStartPoint() const;
template EditorDOMPoint EditorBase::GetLastIMESelectionEndPoint() const;
template EditorRawDOMPoint EditorBase::GetLastIMESelectionEndPoint() const;
template Result<CreateContentResult, nsresult>
EditorBase::InsertNodeWithTransaction(nsIContent& aContentToInsert,
const EditorDOMPoint& aPointToInsert);
template Result<CreateElementResult, nsresult>
EditorBase::InsertNodeWithTransaction(Element& aContentToInsert,
const EditorDOMPoint& aPointToInsert);
template Result<CreateTextResult, nsresult>
EditorBase::InsertNodeWithTransaction(Text& aContentToInsert,
const EditorDOMPoint& aPointToInsert);
template EditorDOMPoint EditorBase::GetFirstSelectionStartPoint() const;
template EditorRawDOMPoint EditorBase::GetFirstSelectionStartPoint() const;
template EditorDOMPoint EditorBase::GetFirstSelectionEndPoint() const;
template EditorRawDOMPoint EditorBase::GetFirstSelectionEndPoint() const;
template EditorDOMPoint EditorBase::FindBetterInsertionPoint(
const EditorDOMPoint& aPoint) const;
template EditorRawDOMPoint EditorBase::FindBetterInsertionPoint(
const EditorRawDOMPoint& aPoint) const;
template EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager(
const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount,
const EditorDOMPoint& aPointAtCaret);
template EditorBase::AutoCaretBidiLevelManager::AutoCaretBidiLevelManager(
const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount,
const EditorRawDOMPoint& aPointAtCaret);
EditorBase::EditorBase(EditorType aEditorType)
: mEditActionData(nullptr),
mPlaceholderName(nullptr),
mModCount(0),
mFlags(0),
mUpdateCount(0),
mPlaceholderBatch(0),
mWrapColumn(0),
mNewlineHandling(StaticPrefs::editor_singleLine_pasteNewlines()),
mCaretStyle(StaticPrefs::layout_selection_caret_style()),
mDocDirtyState(-1),
mSpellcheckCheckboxState(eTriUnset),
mInitSucceeded(false),
mAllowsTransactionsToChangeSelection(true),
mDidPreDestroy(false),
mDidPostCreate(false),
mDispatchInputEvent(true),
mIsInEditSubAction(false),
mHidingCaret(false),
mSpellCheckerDictionaryUpdated(true),
mIsHTMLEditorClass(aEditorType == EditorType::HTML) {
#ifdef XP_WIN
if (!mCaretStyle && !IsTextEditor()) {
// Wordpad-like caret behavior.
mCaretStyle = 1;
}
#endif // #ifdef XP_WIN
if (mNewlineHandling < nsIEditor::eNewlinesPasteIntact ||
mNewlineHandling > nsIEditor::eNewlinesStripSurroundingWhitespace) {
mNewlineHandling = nsIEditor::eNewlinesPasteToFirst;
}
}
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)
// Remove event listeners first since EditorEventListener may need
// mDocument, mEventTarget, etc.
if (tmp->mEventListener) {
tmp->mEventListener->Disconnect();
tmp->mEventListener = nullptr;
}
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(mDocStateListeners)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEventTarget)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlaceholderTransaction)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder)
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EditorBase)
Document* 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(mDocStateListeners)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventTarget)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventListener)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlaceholderTransaction)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder)
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::InitInternal(Document& aDocument, Element* aRootElement,
nsISelectionController& aSelectionController,
uint32_t aFlags) {
MOZ_ASSERT_IF(
!mEditActionData ||
!mEditActionData->HasEditorDestroyedDuringHandlingEditAction(),
GetTopLevelEditSubAction() == EditSubAction::eNone);
// First only set flags, but other stuff shouldn't be initialized now.
// Note that SetFlags() will be called by PostCreate().
mFlags = aFlags;
mDocument = &aDocument;
// nsISelectionController should be stored only when we're a `TextEditor`.
// Otherwise, in `HTMLEditor`, it's `PresShell`, and grabbing it causes
// a circular reference and memory leak.
// XXX Should we move `mSelectionController to `TextEditor`?
MOZ_ASSERT_IF(!IsTextEditor(), &aSelectionController == GetPresShell());
if (IsTextEditor()) {
MOZ_ASSERT(&aSelectionController != GetPresShell());
mSelectionController = &aSelectionController;
}
if (mEditActionData) {
// During edit action, selection is cached. But this selection is invalid
// now since selection controller is updated, so we have to update this
// cache.
Selection* selection = aSelectionController.GetSelection(
nsISelectionController::SELECTION_NORMAL);
NS_WARNING_ASSERTION(selection,
"SelectionController::GetSelection() failed");
if (selection) {
mEditActionData->UpdateSelectionCache(*selection);
}
}
// set up root element if we are passed one.
if (aRootElement) {
mRootElement = aRootElement;
}
// 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.
DebugOnly<nsresult> rvIgnored = aSelectionController.SetCaretReadOnly(false);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"nsISelectionController::SetCaretReadOnly(false) failed, but ignored");
// Show all the selection reflected to user.
rvIgnored =
aSelectionController.SetSelectionFlags(nsISelectionDisplay::DISPLAY_ALL);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"nsISelectionController::SetSelectionFlags("
"nsISelectionDisplay::DISPLAY_ALL) failed, but ignored");
MOZ_ASSERT(IsInitialized());
AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_FAILURE;
}
SelectionRef().AddSelectionListener(this);
// Make sure that the editor will be destroyed properly
mDidPreDestroy = false;
// Make sure that the editor will be created properly
mDidPostCreate = false;
return NS_OK;
}
nsresult EditorBase::EnsureEmptyTextFirstChild() {
MOZ_ASSERT(IsTextEditor());
RefPtr<Element> root = GetRoot();
nsIContent* firstChild = root->GetFirstChild();
if (!firstChild || !firstChild->IsText()) {
RefPtr<nsTextNode> newTextNode = CreateTextNode(u""_ns);
if (!newTextNode) {
NS_WARNING("EditorBase::CreateTextNode() failed");
return NS_ERROR_UNEXPECTED;
}
IgnoredErrorResult ignoredError;
root->InsertChildBefore(newTextNode, root->GetFirstChild(), true,
ignoredError);
MOZ_ASSERT(!ignoredError.Failed());
}
return NS_OK;
}
nsresult EditorBase::PostCreateInternal() {
MOZ_ASSERT(IsEditActionDataAvailable());
// 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);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::SetFlags() failed");
return EditorBase::ToGenericNSResult(rv);
}
// These operations only need to happen on the first PostCreate call
if (!mDidPostCreate) {
mDidPostCreate = true;
// Set up listeners
CreateEventListeners();
nsresult rv = InstallEventListeners();
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::InstallEventListeners() failed");
return EditorBase::ToGenericNSResult(rv);
}
// nuke the modification count, so the doc appears unmodified
// do this before we notify listeners
DebugOnly<nsresult> rvIgnored = ResetModificationCount();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::ResetModificationCount() failed, but ignored");
// update the UI with our state
rvIgnored = NotifyDocumentListeners(eDocumentCreated);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::NotifyDocumentListeners(eDocumentCreated)"
" failed, but ignored");
rvIgnored = NotifyDocumentListeners(eDocumentStateChanged);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::NotifyDocumentListeners("
"eDocumentStateChanged) failed, but ignored");
}
// update nsTextStateManager and caret if we have focus
if (RefPtr<Element> focusedElement = GetFocusedElement()) {
DebugOnly<nsresult> rvIgnored = InitializeSelection(*focusedElement);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::InitializeSelection() failed, but ignored");
// 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.
nsresult rv = FlushPendingSpellCheck();
if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
NS_WARNING(
"EditorBase::FlushPendingSpellCheck() caused destroying the editor");
return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
}
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::FlushPendingSpellCheck() failed, but ignored");
IMEState newState;
rv = GetPreferredIMEState(&newState);
if (NS_FAILED(rv)) {
NS_WARNING("EditorBase::GetPreferredIMEState() failed");
return NS_OK;
}
IMEStateManager::UpdateIMEState(newState, focusedElement, *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.
mEventTarget = GetExposedRoot();
if (NS_WARN_IF(!mEventTarget)) {
return NS_ERROR_NOT_AVAILABLE;
}
nsresult rv = mEventListener->Connect(this);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorEventListener::Connect() failed");
if (mComposition) {
// If mComposition has already been destroyed, we should forget it.
// This may happen if it ended while we don't listen to composition
// events.
if (mComposition->Destroyed()) {
// XXX We may need to fix existing composition transaction here.
// However, this may be called when it's not safe.
// Perhaps, we should stop handling composition with events.
mComposition = nullptr;
}
// Otherwise, Restart to handle composition with new editor contents.
else {
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::IsListeningToEvents() const {
return IsInitialized() && mEventListener &&
!mEventListener->DetachedFromEditor();
}
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;
}
PresShell* 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 (!IsInPlaintextMode()) {
// 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.
Document* doc = content->GetComposedDoc();
return doc && doc->IsEditingOn();
}
return element->Spellcheck();
}
void EditorBase::PreDestroyInternal() {
MOZ_ASSERT(!mDidPreDestroy);
mInitSucceeded = false;
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) {
DebugOnly<nsresult> rvIgnored =
mInlineSpellChecker->Cleanup(IsTextEditor());
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"mozInlineSpellChecker::Cleanup() failed, but ignored");
}
// tell our listeners that the doc is going away
DebugOnly<nsresult> rvIgnored =
NotifyDocumentListeners(eDocumentToBeDestroyed);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::NotifyDocumentListeners("
"eDocumentToBeDestroyed) failed, but ignored");
// Unregister event listeners
RemoveEventListeners();
// If this editor is still hiding the caret, we need to restore it.
HideCaret(false);
mActionListeners.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,
"EditorBase::DisableUndoRedo() failed, but ignored");
mTransactionManager = nullptr;
}
if (mEditActionData) {
mEditActionData->OnEditorDestroy();
}
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;
}
// If we're a `TextEditor` instance, the plaintext mode should always be set.
// If we're an `HTMLEditor` instance, either is fine.
MOZ_ASSERT_IF(IsTextEditor(), !!(aFlags & nsIEditor::eEditorPlaintextMask));
// If we're an `HTMLEditor` instance, we cannot treat it as a single line
// editor. So, eEditorSingleLineMask is available only when we're a
// `TextEditor` instance.
MOZ_ASSERT_IF(IsHTMLEditor(), !(aFlags & nsIEditor::eEditorSingleLineMask));
// If we're an `HTMLEditor` instance, we cannot treat it as a password editor.
// So, eEditorPasswordMask is available only when we're a `TextEditor`
// instance.
MOZ_ASSERT_IF(IsHTMLEditor(), !(aFlags & nsIEditor::eEditorPasswordMask));
// eEditorAllowInteraction changes the behavior of `HTMLEditor`. So, it's
// not available with `TextEditor` instance.
MOZ_ASSERT_IF(IsTextEditor(), !(aFlags & nsIEditor::eEditorAllowInteraction));
const bool isCalledByPostCreate = (mFlags == ~aFlags);
// We don't support dynamic password flag change.
MOZ_ASSERT_IF(!isCalledByPostCreate,
!((mFlags ^ aFlags) & nsIEditor::eEditorPasswordMask));
bool spellcheckerWasEnabled = !isCalledByPostCreate && 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.
if (RefPtr<Element> focusedElement = GetFocusedElement()) {
IMEState newState;
nsresult rv = GetPreferredIMEState(&newState);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::GetPreferredIMEState() failed, but ignored");
if (NS_SUCCEEDED(rv)) {
// NOTE: When the enabled state isn't going to be modified, this method
// is going to do nothing.
IMEStateManager::UpdateIMEState(newState, focusedElement, *this);
}
}
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetIsSelectionEditable(bool* aIsSelectionEditable) {
if (NS_WARN_IF(!aIsSelectionEditable)) {
return NS_ERROR_INVALID_ARG;
}
*aIsSelectionEditable = IsSelectionEditable();
return NS_OK;
}
bool EditorBase::IsSelectionEditable() {
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return false;
}
if (IsTextEditor()) {
// XXX we just check that the anchor node is editable at the moment
// we should check that all nodes in the selection are editable
const nsINode* anchorNode = SelectionRef().GetAnchorNode();
return anchorNode && anchorNode->IsContent() && anchorNode->IsEditable();
}
const nsINode* anchorNode = SelectionRef().GetAnchorNode();
const nsINode* focusNode = SelectionRef().GetFocusNode();
if (!anchorNode || !focusNode) {
return false;
}
// if anchorNode or focusNode is in a native anonymous subtree, HTMLEditor
// shouldn't edit content in it.
// XXX This must be a bug of Selection API.
if (MOZ_UNLIKELY(anchorNode->IsInNativeAnonymousSubtree() ||
focusNode->IsInNativeAnonymousSubtree())) {
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 = SelectionRef().RangeCount() &&
anchorNode->IsEditable() &&
focusNode->IsEditable();
if (!isSelectionEditable) {
return false;
}
const nsINode* commonAncestor =
SelectionRef().GetAnchorFocusRange()->GetClosestCommonInclusiveAncestor();
while (commonAncestor && !commonAncestor->IsEditable()) {
commonAncestor = commonAncestor->GetParentNode();
}
// If there is no editable common ancestor, return false.
return !!commonAncestor;
}
NS_IMETHODIMP EditorBase::GetIsDocumentEditable(bool* aIsDocumentEditable) {
if (NS_WARN_IF(!aIsDocumentEditable)) {
return NS_ERROR_INVALID_ARG;
}
RefPtr<Document> document = GetDocument();
*aIsDocumentEditable = document && IsModifiable();
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetDocument(Document** aDocument) {
if (NS_WARN_IF(!aDocument)) {
return NS_ERROR_INVALID_ARG;
}
*aDocument = do_AddRef(mDocument).take();
return NS_WARN_IF(!*aDocument) ? NS_ERROR_NOT_INITIALIZED : NS_OK;
}
already_AddRefed<nsIWidget> EditorBase::GetWidget() const {
nsPresContext* presContext = GetPresContext();
if (NS_WARN_IF(!presContext)) {
return nullptr;
}
nsCOMPtr<nsIWidget> widget = presContext->GetRootWidget();
return NS_WARN_IF(!widget) ? nullptr : widget.forget();
}
NS_IMETHODIMP EditorBase::GetContentsMIMEType(nsAString& aContentsMIMEType) {
aContentsMIMEType = mContentMIMEType;
return NS_OK;
}
NS_IMETHODIMP EditorBase::SetContentsMIMEType(
const nsAString& aContentsMIMEType) {
mContentMIMEType.Assign(aContentsMIMEType);
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetSelectionController(
nsISelectionController** aSelectionController) {
if (NS_WARN_IF(!aSelectionController)) {
return NS_ERROR_INVALID_ARG;
}
*aSelectionController = do_AddRef(GetSelectionController()).take();
return NS_WARN_IF(!*aSelectionController) ? NS_ERROR_FAILURE : NS_OK;
}
NS_IMETHODIMP EditorBase::DeleteSelection(EDirection aAction,
EStripWrappers aStripWrappers) {
nsresult rv = DeleteSelectionAsAction(aAction, aStripWrappers);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::DeleteSelectionAsAction() failed");
return rv;
}
NS_IMETHODIMP EditorBase::GetSelection(Selection** aSelection) {
nsresult rv = GetSelection(SelectionType::eNormal, aSelection);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rv),
"EditorBase::GetSelection(SelectionType::eNormal) failed");
return rv;
}
nsresult EditorBase::GetSelection(SelectionType aSelectionType,
Selection** aSelection) const {
if (NS_WARN_IF(!aSelection)) {
return NS_ERROR_INVALID_ARG;
}
if (IsEditActionDataAvailable()) {
*aSelection = do_AddRef(&SelectionRef()).take();
return NS_OK;
}
nsISelectionController* selectionController = GetSelectionController();
if (NS_WARN_IF(!selectionController)) {
*aSelection = nullptr;
return NS_ERROR_NOT_INITIALIZED;
}
*aSelection = do_AddRef(selectionController->GetSelection(
ToRawSelectionType(aSelectionType)))
.take();
return NS_WARN_IF(!*aSelection) ? NS_ERROR_FAILURE : NS_OK;
}
nsresult EditorBase::DoTransactionInternal(nsITransaction* aTransaction) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(!ShouldAlreadyHaveHandledBeforeInputEventDispatching(),
"beforeinput event hasn't been dispatched yet");
if (mPlaceholderBatch && !mPlaceholderTransaction) {
MOZ_DIAGNOSTIC_ASSERT(mPlaceholderName);
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
RefPtr<PlaceholderTransaction> placeholderTransaction =
mPlaceholderTransaction;
DebugOnly<nsresult> rvIgnored =
DoTransactionInternal(placeholderTransaction);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::DoTransactionInternal() failed, but ignored");
if (mTransactionManager) {
if (nsCOMPtr<nsITransaction> topTransaction =
mTransactionManager->PeekUndoStack()) {
if (RefPtr<EditTransactionBase> topTransactionBase =
topTransaction->GetAsEditTransactionBase()) {
if (PlaceholderTransaction* topPlaceholderTransaction =
topTransactionBase->GetAsPlaceholderTransaction()) {
// 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 (aTransaction) {
// 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
SelectionBatcher selectionBatcher(SelectionRef(), __FUNCTION__);
if (mTransactionManager) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
nsresult rv = transactionManager->DoTransaction(aTransaction);
if (NS_FAILED(rv)) {
NS_WARNING("TransactionManager::DoTransaction() failed");
return rv;
}
} else {
nsresult rv = aTransaction->DoTransaction();
if (NS_FAILED(rv)) {
NS_WARNING("nsITransaction::DoTransaction() failed");
return rv;
}
}
DoAfterDoTransaction(aTransaction);
}
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,
"EditorBase::EnableUndoRedo() failed, but ignored");
return NS_OK;
}
DebugOnly<bool> disabledUndoRedo = DisableUndoRedo();
NS_WARNING_ASSERTION(disabledUndoRedo,
"EditorBase::DisableUndoRedo() failed, but ignored");
return NS_OK;
}
NS_IMETHODIMP EditorBase::ClearUndoRedoXPCOM() {
if (MOZ_UNLIKELY(!ClearUndoRedo())) {
return NS_ERROR_FAILURE; // We're handling a transaction
}
return NS_OK;
}
NS_IMETHODIMP EditorBase::Undo() {
nsresult rv = UndoAsAction(1u);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
return rv;
}
NS_IMETHODIMP EditorBase::UndoAll() {
if (!mTransactionManager) {
return NS_OK;
}
size_t numberOfUndoItems = mTransactionManager->NumberOfUndoItems();
if (!numberOfUndoItems) {
return NS_OK; // no transactions
}
nsresult rv = UndoAsAction(numberOfUndoItems);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
return rv;
}
NS_IMETHODIMP EditorBase::GetUndoRedoEnabled(bool* aIsEnabled) {
MOZ_ASSERT(aIsEnabled);
*aIsEnabled = IsUndoRedoEnabled();
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetCanUndo(bool* aCanUndo) {
MOZ_ASSERT(aCanUndo);
*aCanUndo = CanUndo();
return NS_OK;
}
NS_IMETHODIMP EditorBase::Redo() {
nsresult rv = RedoAsAction(1u);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RedoAsAction() failed");
return rv;
}
NS_IMETHODIMP EditorBase::GetCanRedo(bool* aCanRedo) {
MOZ_ASSERT(aCanRedo);
*aCanRedo = CanRedo();
return NS_OK;
}
nsresult EditorBase::UndoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) {
if (aCount == 0 || IsReadonly()) {
return NS_OK;
}
// If we don't have transaction in the undo stack, we shouldn't notify
// anybody of trying to undo since it's not useful notification but we
// need to pay some runtime cost.
if (!CanUndo()) {
return NS_OK;
}
// If there is composition, we shouldn't allow to undo with committing
// composition since Chrome doesn't allow it and it doesn't make sense
// because committing composition causes one transaction and Undo(1)
// undoes the committing composition.
if (GetComposition()) {
return NS_OK;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eUndo, aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent() failed");
return EditorBase::ToGenericNSResult(rv);
}
AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
NotifyEditorObservers(eNotifyEditorObserversOfBefore);
if (NS_WARN_IF(!CanUndo()) || NS_WARN_IF(Destroyed())) {
return NS_ERROR_FAILURE;
}
rv = NS_OK;
{
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eUndo, nsIEditor::eNone, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return EditorBase::ToGenericNSResult(ignoredError.StealNSResult());
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"TextEditor::OnStartToHandleTopLevelEditSubAction() "
"failed, but ignored");
RefPtr<TransactionManager> transactionManager(mTransactionManager);
for (uint32_t i = 0; i < aCount; ++i) {
if (NS_FAILED(transactionManager->Undo())) {
NS_WARNING("TransactionManager::Undo() failed");
break;
}
DoAfterUndoTransaction();
}
if (IsHTMLEditor()) {
rv = AsHTMLEditor()->ReflectPaddingBRElementForEmptyEditor();
}
}
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
return EditorBase::ToGenericNSResult(rv);
}
nsresult EditorBase::RedoAsAction(uint32_t aCount, nsIPrincipal* aPrincipal) {
if (aCount == 0 || IsReadonly()) {
return NS_OK;
}
// If we don't have transaction in the redo stack, we shouldn't notify
// anybody of trying to redo since it's not useful notification but we
// need to pay some runtime cost.
if (!CanRedo()) {
return NS_OK;
}
// If there is composition, we shouldn't allow to redo with committing
// composition since Chrome doesn't allow it and it doesn't make sense
// because committing composition causes removing all transactions from
// the redo queue. So, it becomes impossible to redo anything.
if (GetComposition()) {
return NS_OK;
}
AutoEditActionDataSetter editActionData(*this, EditAction::eRedo, aPrincipal);
nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
if (NS_FAILED(rv)) {
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
"CanHandleAndMaybeDispatchBeforeInputEvent() failed");
return EditorBase::ToGenericNSResult(rv);
}
AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
NotifyEditorObservers(eNotifyEditorObserversOfBefore);
if (NS_WARN_IF(!CanRedo()) || NS_WARN_IF(Destroyed())) {
return NS_ERROR_FAILURE;
}
rv = NS_OK;
{
IgnoredErrorResult ignoredError;
AutoEditSubActionNotifier startToHandleEditSubAction(
*this, EditSubAction::eRedo, nsIEditor::eNone, ignoredError);
if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
return ignoredError.StealNSResult();
}
NS_WARNING_ASSERTION(!ignoredError.Failed(),
"TextEditor::OnStartToHandleTopLevelEditSubAction() "
"failed, but ignored");
RefPtr<TransactionManager> transactionManager(mTransactionManager);
for (uint32_t i = 0; i < aCount; ++i) {
if (NS_FAILED(transactionManager->Redo())) {
NS_WARNING("TransactionManager::Redo() failed");
break;
}
DoAfterRedoTransaction();
}
if (IsHTMLEditor()) {
rv = AsHTMLEditor()->ReflectPaddingBRElementForEmptyEditor();
}
}
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
return EditorBase::ToGenericNSResult(rv);
}
NS_IMETHODIMP EditorBase::BeginTransaction() {
AutoEditActionDataSetter editActionData(*this, EditAction::eUnknown);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_FAILURE;
}
BeginTransactionInternal(__FUNCTION__);
return NS_OK;
}
void EditorBase::BeginTransactionInternal(const char* aRequesterFuncName) {
BeginUpdateViewBatch(aRequesterFuncName);
if (NS_WARN_IF(!mTransactionManager)) {
return;
}
RefPtr<TransactionManager> transactionManager(mTransactionManager);
DebugOnly<nsresult> rvIgnored = transactionManager->BeginBatch(nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"TransactionManager::BeginBatch() failed, but ignored");
}
NS_IMETHODIMP EditorBase::EndTransaction() {
AutoEditActionDataSetter editActionData(*this, EditAction::eUnknown);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_FAILURE;
}
EndTransactionInternal(__FUNCTION__);
return NS_OK;
}
void EditorBase::EndTransactionInternal(const char* aRequesterFuncName) {
if (mTransactionManager) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
DebugOnly<nsresult> rvIgnored = transactionManager->EndBatch(false);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"TransactionManager::EndBatch() failed, but ignored");
}
EndUpdateViewBatch(aRequesterFuncName);
}
void EditorBase::BeginPlaceholderTransaction(nsStaticAtom& aTransactionName,
const char* aRequesterFuncName) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(mPlaceholderBatch >= 0, "negative placeholder batch count!");
if (!mPlaceholderBatch) {
NotifyEditorObservers(eNotifyEditorObserversOfBefore);
// time to turn on the batch
BeginUpdateViewBatch(aRequesterFuncName);
mPlaceholderTransaction = nullptr;
mPlaceholderName = &aTransactionName;
mSelState.emplace();
mSelState->SaveSelection(SelectionRef());
// 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) {
RangeUpdaterRef().RegisterSelectionState(*mSelState);
}
}
mPlaceholderBatch++;
}
void EditorBase::EndPlaceholderTransaction(
ScrollSelectionIntoView aScrollSelectionIntoView,
const char* aRequesterFuncName) {
MOZ_ASSERT(IsEditActionDataAvailable());
MOZ_ASSERT(mPlaceholderBatch > 0,
"zero or negative placeholder batch count when ending batch!");
if (!(--mPlaceholderBatch)) {
// By making the assumption that no reflow happens during the calls
// to EndUpdateViewBatch and ScrollSelectionFocusIntoView, 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.
SelectionRef().SetCanCacheFrameOffset(true);
// time to turn off the batch
EndUpdateViewBatch(aRequesterFuncName);
// make sure selection is in view
// After ScrollSelectionFocusIntoView(), the pending notifications might be
// flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
// XXX Even if we're destroyed, we need to keep handling below because
// this method changes a lot of status. We should rewrite this safer.
if (aScrollSelectionIntoView == ScrollSelectionIntoView::Yes) {
DebugOnly<nsresult> rvIgnored = ScrollSelectionFocusIntoView();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::ScrollSelectionFocusIntoView() failed, but Ignored");
}
// cached for frame offset are Not available now
SelectionRef().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) {
RangeUpdaterRef().DropSelectionState(*mSelState);
}
mSelState.reset();
}
// We might have never made a placeholder if no action took place.
if (mPlaceholderTransaction) {
// FYI: Disconnect placeholder transaction before dispatching "input"
// event because an input event listener may start other things.
// TODO: We should forget EditActionDataSetter too.
RefPtr<PlaceholderTransaction> placeholderTransaction =
std::move(mPlaceholderTransaction);
DebugOnly<nsresult> rvIgnored =
placeholderTransaction->EndPlaceHolderBatch();
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"PlaceholderTransaction::EndPlaceHolderBatch() failed, but ignored");
// notify editor observers of action but if composing, it's done by
// compositionchange event handler.
if (!mComposition) {
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
}
} else {
NotifyEditorObservers(eNotifyEditorObserversOfCancel);
}
}
}
NS_IMETHODIMP EditorBase::SetShouldTxnSetSelection(bool aShould) {
MakeThisAllowTransactionsToChangeSelection(aShould);
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetDocumentIsEmpty(bool* aDocumentIsEmpty) {
MOZ_ASSERT(aDocumentIsEmpty);
*aDocumentIsEmpty = IsEmpty();
return NS_OK;
}
// XXX: The rule system should tell us which node to select all on (ie, the
// root, or the body)
NS_IMETHODIMP EditorBase::SelectAll() {
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv = SelectAllInternal();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "SelectAllInternal() failed");
// This is low level API for XUL applcation. So, we should return raw
// error code here.
return rv;
}
nsresult EditorBase::SelectAllInternal() {
MOZ_ASSERT(IsInitialized());
DebugOnly<nsresult> rvIgnored = CommitComposition();
if (NS_WARN_IF(Destroyed())) {
return NS_ERROR_EDITOR_DESTROYED;
}
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"EditorBase::CommitComposition() failed, but ignored");
// 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.
nsresult rv = SelectEntireDocument();
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::SelectEntireDocument() failed");
return rv;
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP EditorBase::BeginningOfDocument() {
MOZ_ASSERT(IsTextEditor());
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
// get the root element
RefPtr<Element> rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_NULL_POINTER;
}
// find first editable thingy
nsCOMPtr<nsIContent> firstEditableLeaf;
// If we're `TextEditor`, the first editable leaf node is a text node or
// padding `<br>` element. In the first case, we need to collapse selection
// into it.
if (rootElement->GetFirstChild() && rootElement->GetFirstChild()->IsText()) {
firstEditableLeaf = rootElement->GetFirstChild();
}
if (!firstEditableLeaf) {
// just the root node, set selection to inside the root
nsresult rv = CollapseSelectionToStartOf(*rootElement);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionToStartOf() failed");
return rv;
}
if (firstEditableLeaf->IsText()) {
// If firstEditableLeaf is text, set selection to beginning of the text
// node.
nsresult rv = CollapseSelectionToStartOf(*firstEditableLeaf);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionToStartOf() failed");
return rv;
}
// Otherwise, it's a leaf node and we set the selection just in front of it.
nsCOMPtr<nsIContent> parent = firstEditableLeaf->GetParent();
if (NS_WARN_IF(!parent)) {
return NS_ERROR_NULL_POINTER;
}
MOZ_ASSERT(
parent->ComputeIndexOf(firstEditableLeaf).valueOr(UINT32_MAX) == 0,
"How come the first node isn't the left most child in its parent?");
nsresult rv = CollapseSelectionToStartOf(*parent);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::CollapseSelectionToStartOf() failed");
return rv;
}
NS_IMETHODIMP EditorBase::EndOfDocument() { return NS_ERROR_NOT_IMPLEMENTED; }
NS_IMETHODIMP EditorBase::GetDocumentModified(bool* aOutDocModified) {
if (NS_WARN_IF(!aOutDocModified)) {
return NS_ERROR_INVALID_ARG;
}
int32_t modCount = 0;
DebugOnly<nsresult> rvIgnored = GetModificationCount(&modCount);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"EditorBase::GetModificationCount() failed, but ignored");
*aOutDocModified = (modCount != 0);
return NS_OK;
}
NS_IMETHODIMP EditorBase::GetDocumentCharacterSet(nsACString& aCharacterSet) {
return NS_ERROR_NOT_AVAILABLE;
}
nsresult EditorBase::GetDocumentCharsetInternal(nsACString& aCharset) const {
Document* document = GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_NOT_INITIALIZED;
}
document->GetDocumentCharacterSet()->Name(aCharset);
return NS_OK;
}
NS_IMETHODIMP EditorBase::SetDocumentCharacterSet(
const nsACString& aCharacterSet) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP EditorBase::OutputToString(const nsAString& aFormatType,
uint32_t aDocumentEncoderFlags,
nsAString& aOutputString) {
AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
if (NS_WARN_IF(!editActionData.CanHandle())) {
return NS_ERROR_NOT_INITIALIZED;
}
nsresult rv =
ComputeValueInternal(aFormatType, aDocumentEncoderFlags, aOutputString);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"EditorBase::ComputeValueInternal() failed");
// This is low level API for XUL application. So, we should return raw
// error code here.
return rv;
}
nsresult EditorBase::ComputeValueInternal(const nsAString& aFormatType,
uint32_t aDocumentEncoderFlags,
nsAString& aOutputString) const {
MOZ_ASSERT(IsEditActionDataAvailable());
// First, let's try to get the value simply only from text node if the
// caller wants plaintext value.
if (aFormatType.LowerCaseEqualsLiteral("text/plain") &&
!(aDocumentEncoderFlags & (nsIDocumentEncoder::OutputSelectionOnly |
nsIDocumentEncoder::OutputWrap))) {
// Shortcut for empty editor case.
if (IsEmpty()) {
aOutputString.Truncate();
return NS_OK;
}
// NOTE: If it's neither <input type="text"> nor <textarea>, e.g., an HTML
// editor which is in plaintext mode (e.g., plaintext email composer on
// Thunderbird), it should be handled by the expensive path.
if (IsTextEditor()) {
// If it's necessary to check selection range or the editor wraps hard,
// we need some complicated handling. In such case, we need to use the
// expensive path.
// XXX Anything else what we cannot return the text node data simply?
Result<EditActionResult, nsresult> result =
AsTextEditor()->ComputeValueFromTextNodeAndBRElement(aOutputString);
if (MOZ_UNLIKELY(result.isErr())) {
NS_WARNING("TextEditor::ComputeValueFromTextNodeAndBRElement() failed");
return result.unwrapErr();
}
if (!result.inspect().Ignored()) {
return NS_OK;
}
}
}
nsAutoCString charset;
nsresult rv = GetDocumentCharsetInternal(charset);
if (NS_FAILED(rv) || charset.IsEmpty()) {
charset.AssignLiteral("windows-1252"); // XXX Why don't we use "UTF-8"?
}
nsCOMPtr<nsIDocumentEncoder> encoder =
GetAndInitDocEncoder(aFormatType, aDocumentEncoderFlags, charset);
if (!encoder) {
NS_WARNING("EditorBase::GetAndInitDocEncoder() failed");
return NS_ERROR_FAILURE;
}
rv = encoder->EncodeToString(aOutputString);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"nsIDocumentEncoder::EncodeToString() failed");
return rv;
}
already_AddRefed<nsIDocumentEncoder> EditorBase::GetAndInitDocEncoder(
const nsAString& aFormatType, uint32_t aDocumentEncoderFlags,
const nsACString& aCharset) const {
MOZ_ASSERT(IsEditActionDataAvailable());
nsCOMPtr<nsIDocumentEncoder> docEncoder;
if (!mCachedDocumentEncoder ||
!mCachedDocumentEncoderType.Equals(aFormatType)) {
nsAutoCString formatType;
LossyAppendUTF16toASCII(aFormatType, formatType);
docEncoder = do_createDocumentEncoder(PromiseFlatCString(formatType).get());
if (NS_WARN_IF(!docEncoder)) {
return nullptr;
}
mCachedDocumentEncoder = docEncoder;
mCachedDocumentEncoderType = aFormatType;
} else {
docEncoder = mCachedDocumentEncoder;
}
RefPtr<Document> doc = GetDocument();
NS_ASSERTION(doc, "Need a document");
nsresult rv = docEncoder->NativeInit(
doc, aFormatType,
aDocumentEncoderFlags | nsIDocumentEncoder::RequiresReinitAfterOutput);
if (NS_FAILED(rv)) {
NS_WARNING("nsIDocumentEncoder::NativeInit() failed");
return nullptr;
}
if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) {
DebugOnly<nsresult> rvIgnored = docEncoder->SetCharset(aCharset);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"nsIDocumentEncoder::SetCharset() failed, but ignored");
}
const int32_t wrapWidth = std::max(WrapWidth(), 0);
DebugOnly<nsresult> rvIgnored = docEncoder->SetWrapColumn(wrapWidth);
NS_WARNING_ASSERTION(
NS_SUCCEEDED(rvIgnored),
"nsIDocumentEncoder::SetWrapColumn() failed, but ignored");
// Set the selection, if appropriate.
// We do this either if the OutputSelectionOnly flag is set,
// in which case we use our existing selection ...
if (aDocumentEncoderFlags & nsIDocumentEncoder::OutputSelectionOnly) {
if (NS_FAILED(docEncoder->SetSelection(&SelectionRef()))) {
NS_WARNING("nsIDocumentEncoder::SetSelection() failed");
return nullptr;
}
}
// ... or if the root element is not a body,
// in which case we set the selection to encompass the root.
else {
Element* rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return nullptr;
}
if (!rootElement->IsHTMLElement(nsGkAtoms::body)) {
if (NS_FAILED(docEncoder->SetContainerNode(rootElement))) {
NS_WARNING("nsIDocumentEncoder::SetContainerNode() failed");
return nullptr;
}
}
}
return docEncoder.forget();
}
bool EditorBase::AreClipboardCommandsUnconditionallyEnabled() const {
Document* document = GetDocument();
return document && document->AreClipboardCommandsUnconditionallyEnabled();
}
bool EditorBase::CheckForClipboardCommandListener(
nsAtom* aCommand, EventMessage aEventMessage) const {
RefPtr<Document> document = GetDocument();
if (!document) {
return false;
}
// We exclude XUL and chrome docs here to maintain current behavior where
// in these cases the editor element alone is expected to handle clipboard
// command availability.
if (!document->AreClipboardCommandsUnconditionallyEnabled()) {
return false;
}
// So in web content documents, "unconditionally" enabled Cut/Copy are not
// really unconditional; they're enabled if there is a listener that wants
// to handle them. What they're not conditional on here is whether there is
// currently a selection in the editor.
RefPtr<PresShell> presShell = document->GetObservingPresShell();
if (!presShell) {
return false;
}
RefPtr<nsPresContext> presContext = presShell->GetPresContext();
if (!presContext) {
return false;
}
RefPtr<EventTarget> et = GetDOMEventTarget();
while (et) {
EventListenerManager* elm = et->GetExistingListenerManager();
if (elm && elm->HasListenersFor(aCommand)) {
return true;
}
InternalClipboardEvent event(true, aEventMessage);
EventChainPreVisitor visitor(presContext, &event, nullptr,
nsEventStatus_eIgnore, false, et);
et->GetEventTargetParent(visitor);
et = visitor.GetParentTarget();
}
return false;
}
bool EditorBase::FireClipboardEvent(EventMessage aEventMessage,
int32_t aClipboardType,
bool* aActionTaken) {