summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/TextEditor.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'editor/libeditor/TextEditor.cpp')
-rw-r--r--editor/libeditor/TextEditor.cpp1133
1 files changed, 1133 insertions, 0 deletions
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp
new file mode 100644
index 0000000000..cb9c4d6f96
--- /dev/null
+++ b/editor/libeditor/TextEditor.cpp
@@ -0,0 +1,1133 @@
+/* -*- 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 "TextEditor.h"
+
+#include <algorithm>
+
+#include "EditAction.h"
+#include "EditAggregateTransaction.h"
+#include "EditorDOMPoint.h"
+#include "HTMLEditor.h"
+#include "HTMLEditUtils.h"
+#include "InternetCiter.h"
+#include "PlaceholderTransaction.h"
+#include "gfxFontUtils.h"
+
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/TextEvents.h"
+#include "mozilla/TextServicesDocument.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Selection.h"
+#include "mozilla/dom/StaticRange.h"
+
+#include "nsAString.h"
+#include "nsCRT.h"
+#include "nsCaret.h"
+#include "nsCharTraits.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentCID.h"
+#include "nsContentList.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsINode.h"
+#include "nsIPrincipal.h"
+#include "nsISelectionController.h"
+#include "nsISupportsPrimitives.h"
+#include "nsITransferable.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsNameSpaceManager.h"
+#include "nsLiteralString.h"
+#include "nsPresContext.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTextFragment.h"
+#include "nsTextNode.h"
+#include "nsUnicharUtils.h"
+#include "nsXPCOM.h"
+
+class nsIOutputStream;
+class nsISupports;
+
+namespace mozilla {
+
+using namespace dom;
+
+using LeafNodeType = HTMLEditUtils::LeafNodeType;
+using LeafNodeTypes = HTMLEditUtils::LeafNodeTypes;
+
+TextEditor::TextEditor() : EditorBase(EditorBase::EditorType::Text) {
+ // printf("Size of TextEditor: %zu\n", sizeof(TextEditor));
+ static_assert(
+ sizeof(TextEditor) <= 512,
+ "TextEditor instance should be allocatable in the quantum class bins");
+}
+
+TextEditor::~TextEditor() {
+ // Remove event listeners. Note that if we had an HTML editor,
+ // it installed its own instead of these
+ RemoveEventListeners();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase)
+ if (tmp->mPasswordMaskData) {
+ tmp->mPasswordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::No);
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPasswordMaskData->mTimer)
+ }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase)
+ if (tmp->mPasswordMaskData) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPasswordMaskData->mTimer)
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase)
+NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditor)
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END_INHERITING(EditorBase)
+
+NS_IMETHODIMP TextEditor::EndOfDocument() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ nsresult rv = CollapseSelectionToEndOfTextNode();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::CollapseSelectionToEndOfTextNode() failed");
+ // This is low level API for embedders and chrome script so that we can return
+ // raw error code here.
+ return rv;
+}
+
+nsresult TextEditor::CollapseSelectionToEndOfTextNode() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ Element* anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ RefPtr<Text> textNode =
+ Text::FromNodeOrNull(anonymousDivElement->GetFirstChild());
+ MOZ_ASSERT(textNode);
+ nsresult rv = CollapseSelectionToEndOf(*textNode);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::CollapseSelectionToEndOf() failed");
+ return rv;
+}
+
+nsresult TextEditor::Init(Document& aDocument, Element& aAnonymousDivElement,
+ nsISelectionController& aSelectionController,
+ uint32_t aFlags,
+ UniquePtr<PasswordMaskData>&& aPasswordMaskData) {
+ MOZ_ASSERT(!mInitSucceeded,
+ "TextEditor::Init() called again without calling PreDestroy()?");
+ MOZ_ASSERT(!(aFlags & nsIEditor::eEditorPasswordMask) == !aPasswordMaskData);
+ mPasswordMaskData = std::move(aPasswordMaskData);
+
+ // Init the base editor
+ nsresult rv = InitInternal(aDocument, &aAnonymousDivElement,
+ aSelectionController, aFlags);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InitInternal() failed");
+ return rv;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInitializing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We set mInitSucceeded here rather than at the end of the function,
+ // since InitEditorContentAndSelection() can perform some transactions
+ // and can warn if mInitSucceeded is still false.
+ MOZ_ASSERT(!mInitSucceeded, "TextEditor::Init() shouldn't be nested");
+ mInitSucceeded = true;
+
+ rv = InitEditorContentAndSelection();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::InitEditorContentAndSelection() failed");
+ // XXX Shouldn't we expose `NS_ERROR_EDITOR_DESTROYED` even though this
+ // is a public method?
+ mInitSucceeded = false;
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Throw away the old transaction manager if this is not the first time that
+ // we're initializing the editor.
+ ClearUndoRedo();
+ EnableUndoRedo();
+ return NS_OK;
+}
+
+nsresult TextEditor::InitEditorContentAndSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ MOZ_TRY(EnsureEmptyTextFirstChild());
+
+ // If the selection hasn't been set up yet, set it up collapsed to the end of
+ // our editable content.
+ if (!SelectionRef().RangeCount()) {
+ nsresult rv = CollapseSelectionToEndOfTextNode();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionToEndOfTextNode() failed");
+ return rv;
+ }
+ }
+
+ if (!IsSingleLineEditor()) {
+ nsresult rv = EnsurePaddingBRElementInMultilineEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::EnsurePaddingBRElementInMultilineEditor() failed");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TextEditor::PostCreate() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = PostCreateInternal();
+
+ // Restore unmasked range if there is.
+ if (IsPasswordEditor() && !IsAllMasked()) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRangeAndNotify(UnmaskedStart(), UnmaskedLength());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRangeAndNotify() failed to "
+ "restore unmasked range, but ignored");
+ }
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::PostCreateInternal() failed");
+ return rv;
+}
+
+UniquePtr<PasswordMaskData> TextEditor::PreDestroy() {
+ if (mDidPreDestroy) {
+ return nullptr;
+ }
+
+ UniquePtr<PasswordMaskData> passwordMaskData = std::move(mPasswordMaskData);
+ if (passwordMaskData) {
+ // Disable auto-masking timer since nobody can catch the notification
+ // from the timer and canceling the unmasking.
+ passwordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::Yes);
+ // Similary, keeping preventing echoing password temporarily across
+ // TextEditor instances is hard. So, we should forget it.
+ passwordMaskData->mEchoingPasswordPrevented = false;
+ }
+
+ PreDestroyInternal();
+
+ return passwordMaskData;
+}
+
+nsresult TextEditor::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;
+ }
+
+ if (IsReadonly()) {
+ HandleKeyPressEventInReadOnlyMode(*aKeyboardEvent);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress,
+ "HandleKeyPressEvent gets non-keypress event");
+
+ switch (aKeyboardEvent->mKeyCode) {
+ case NS_VK_META:
+ case NS_VK_WIN:
+ case NS_VK_SHIFT:
+ case NS_VK_CONTROL:
+ case NS_VK_ALT:
+ // FYI: This shouldn't occur since modifier key shouldn't cause eKeyPress
+ // event.
+ aKeyboardEvent->PreventDefault();
+ return NS_OK;
+
+ case NS_VK_BACK:
+ case NS_VK_DELETE:
+ case NS_VK_TAB: {
+ nsresult rv = EditorBase::HandleKeyPressEvent(aKeyboardEvent);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::HandleKeyPressEvent() failed");
+ return rv;
+ }
+ case NS_VK_RETURN: {
+ if (!aKeyboardEvent->IsInputtingLineBreak()) {
+ return NS_OK;
+ }
+ if (!IsSingleLineEditor()) {
+ aKeyboardEvent->PreventDefault();
+ }
+ // We need to dispatch "beforeinput" event at least even if we're a
+ // single line text editor.
+ nsresult rv = InsertLineBreakAsAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertLineBreakAsAction() failed");
+ return rv;
+ }
+ }
+
+ if (!aKeyboardEvent->IsInputtingText()) {
+ // we don't PreventDefault() here or keybindings like control-x won't work
+ return NS_OK;
+ }
+ // Our widget shouldn't set `\r` to `mCharCode`, but it may be synthesized
+ // keyboard event and its value may be `\r`. In such case, we should treat
+ // it as `\n` for the backward compatibility because we stopped converting
+ // `\r` and `\r\n` to `\n` at getting `HTMLInputElement.value` and
+ // `HTMLTextAreaElement.value` for the performance (i.e., we don't need to
+ // take care in `HTMLEditor`).
+ char16_t charCode =
+ static_cast<char16_t>(aKeyboardEvent->mCharCode) == nsCRT::CR
+ ? nsCRT::LF
+ : static_cast<char16_t>(aKeyboardEvent->mCharCode);
+ aKeyboardEvent->PreventDefault();
+ nsAutoString str(charCode);
+ nsresult rv = OnInputText(str);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::OnInputText() failed");
+ return rv;
+}
+
+NS_IMETHODIMP TextEditor::InsertLineBreak() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (NS_WARN_IF(IsSingleLineEditor())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertLineBreakAsSubAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertLineBreakAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::InsertLineBreakAsAction(nsIPrincipal* aPrincipal) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertLineBreak,
+ aPrincipal);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (IsSingleLineEditor()) {
+ return NS_OK;
+ }
+
+ // XXX This may be called by execCommand() with "insertParagraph".
+ // In such case, naming the transaction "TypingTxnName" is odd.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes,
+ __FUNCTION__);
+ rv = InsertLineBreakAsSubAction();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertLineBreakAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::SetTextAsAction(
+ const nsAString& aString,
+ AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetText,
+ aPrincipal);
+ if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) {
+ editActionData.MakeBeforeInputEventNonCancelable();
+ }
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = SetTextAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::SetTextAsSubAction(const nsAString& aString) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mPlaceholderBatch);
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eSetText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ if (!IsIMEComposing() && !IsUndoRedoEnabled() &&
+ GetEditAction() != EditAction::eReplaceText && mMaxTextLength < 0) {
+ Result<EditActionResult, nsresult> result =
+ SetTextWithoutTransaction(aString);
+ if (MOZ_UNLIKELY(result.isErr())) {
+ NS_WARNING("TextEditor::SetTextWithoutTransaction() failed");
+ return result.unwrapErr();
+ }
+ if (!result.inspect().Ignored()) {
+ return NS_OK;
+ }
+ }
+
+ {
+ // Note that do not notify selectionchange caused by selecting all text
+ // because it's preparation of our delete implementation so web apps
+ // shouldn't receive such selectionchange before the first mutation.
+ AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
+
+ // XXX We should make ReplaceSelectionAsSubAction() take range. Then,
+ // we can saving the expensive cost of modifying `Selection` here.
+ if (NS_SUCCEEDED(SelectEntireDocument())) {
+ DebugOnly<nsresult> rvIgnored = ReplaceSelectionAsSubAction(aString);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::ReplaceSelectionAsSubAction() failed, but ignored");
+ }
+ }
+
+ // Destroying AutoUpdateViewBatch may cause destroying us.
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+already_AddRefed<Element> TextEditor::GetInputEventTargetElement() const {
+ RefPtr<Element> target = Element::FromEventTargetOrNull(mEventTarget);
+ return target.forget();
+}
+
+bool TextEditor::IsEmpty() const {
+ // Even if there is no padding <br> element for empty editor, we should be
+ // detected as empty editor if all the children are text nodes and these
+ // have no content.
+ Element* anonymousDivElement = GetRoot();
+ if (!anonymousDivElement) {
+ return true; // Don't warn it, this is possible, e.g., 997805.html
+ }
+
+ MOZ_ASSERT(anonymousDivElement->GetFirstChild() &&
+ anonymousDivElement->GetFirstChild()->IsText());
+
+ // Only when there is non-empty text node, we are not empty.
+ return !anonymousDivElement->GetFirstChild()->Length();
+}
+
+NS_IMETHODIMP TextEditor::GetTextLength(uint32_t* aCount) {
+ MOZ_ASSERT(aCount);
+
+ // initialize out params
+ *aCount = 0;
+
+ // special-case for empty document, to account for the padding <br> element
+ // for empty editor.
+ // XXX This should be overridden by `HTMLEditor` and we should return the
+ // first text node's length from `TextEditor` instead. The following
+ // code is too expensive.
+ if (IsEmpty()) {
+ return NS_OK;
+ }
+
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t totalLength = 0;
+ PostContentIterator postOrderIter;
+ DebugOnly<nsresult> rvIgnored = postOrderIter.Init(rootElement);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "PostContentIterator::Init() failed, but ignored");
+ EditorType editorType = GetEditorType();
+ for (; !postOrderIter.IsDone(); postOrderIter.Next()) {
+ nsINode* currentNode = postOrderIter.GetCurrentNode();
+ if (currentNode && currentNode->IsText() &&
+ EditorUtils::IsEditableContent(*currentNode->AsText(), editorType)) {
+ totalLength += currentNode->Length();
+ }
+ }
+
+ *aCount = totalLength;
+ return NS_OK;
+}
+
+bool TextEditor::IsCopyToClipboardAllowedInternal() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ if (!EditorBase::IsCopyToClipboardAllowedInternal()) {
+ return false;
+ }
+
+ if (!IsSingleLineEditor() || !IsPasswordEditor() ||
+ NS_WARN_IF(!mPasswordMaskData)) {
+ return true;
+ }
+
+ // If we're a password editor, we should allow selected text to be copied
+ // to the clipboard only when selection range is in unmasked range.
+ if (IsAllMasked() || IsMaskingPassword() || !UnmaskedLength()) {
+ return false;
+ }
+
+ // If there are 2 or more ranges, we don't allow to copy/cut for now since
+ // we need to check whether all ranges are in unmasked range or not.
+ // Anyway, such operation in password field does not make sense.
+ if (SelectionRef().RangeCount() > 1) {
+ return false;
+ }
+
+ uint32_t selectionStart = 0, selectionEnd = 0;
+ nsContentUtils::GetSelectionInTextControl(&SelectionRef(), mRootElement,
+ selectionStart, selectionEnd);
+ return UnmaskedStart() <= selectionStart && UnmaskedEnd() >= selectionEnd;
+}
+
+nsresult TextEditor::PasteAsQuotationAsAction(int32_t aClipboardType,
+ bool aDispatchPasteEvent,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
+ aClipboardType == nsIClipboard::kSelectionClipboard);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::ePasteAsQuotation,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ MOZ_ASSERT(GetDocument());
+
+ // Get Clipboard Service
+ nsresult rv;
+ nsCOMPtr<nsIClipboard> clipboard =
+ do_GetService("@mozilla.org/widget/clipboard;1", &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get nsIClipboard service");
+ return rv;
+ }
+
+ // XXX Why don't we dispatch ePaste event here?
+
+ // Get the nsITransferable interface for getting the data from the clipboard
+ Result<nsCOMPtr<nsITransferable>, nsresult> maybeTransferable =
+ EditorUtils::CreateTransferableForPlainText(*GetDocument());
+ if (maybeTransferable.isErr()) {
+ NS_WARNING("EditorUtils::CreateTransferableForPlainText() failed");
+ return EditorBase::ToGenericNSResult(maybeTransferable.unwrapErr());
+ }
+ nsCOMPtr<nsITransferable> trans(maybeTransferable.unwrap());
+ if (!trans) {
+ NS_WARNING(
+ "EditorUtils::CreateTransferableForPlainText() returned nullptr, but "
+ "ignored");
+ return NS_OK;
+ }
+
+ // Get the Data from the clipboard
+ clipboard->GetData(trans, aClipboardType);
+
+ // Now we ask the transferable for the data
+ // it still owns the data, we just have a pointer to it.
+ // If it can't support a "text" output of the data the call will fail
+ nsCOMPtr<nsISupports> genericDataObj;
+ nsAutoCString flav;
+ rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("nsITransferable::GetAnyTransferData() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (!flav.EqualsLiteral(kUnicodeMime) &&
+ !flav.EqualsLiteral(kMozTextInternal)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISupportsString> text = do_QueryInterface(genericDataObj);
+ if (!text) {
+ return NS_OK;
+ }
+
+ nsString stuffToPaste;
+ DebugOnly<nsresult> rvIgnored = text->GetData(stuffToPaste);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsISupportsString::GetData() failed, but ignored");
+ if (stuffToPaste.IsEmpty()) {
+ return NS_OK;
+ }
+
+ editActionData.SetData(stuffToPaste);
+ if (!stuffToPaste.IsEmpty()) {
+ nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste);
+ }
+ // XXX Perhaps, we should dispatch "paste" event with the pasting text data.
+ editActionData.NotifyOfDispatchingClipboardEvent();
+ rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ AutoPlaceholderBatch treatAsOneTransaction(
+ *this, ScrollSelectionIntoView::Yes, __FUNCTION__);
+ rv = InsertWithQuotationsAsSubAction(stuffToPaste);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::InsertWithQuotationsAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::InsertWithQuotationsAsSubAction(
+ const nsAString& aQuotedText) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (IsReadonly()) {
+ return NS_OK;
+ }
+
+ // Let the citer quote it for us:
+ nsString quotedStuff;
+ InternetCiter::GetCiteString(aQuotedText, quotedStuff);
+
+ // It's best to put a blank line after the quoted text so that mails
+ // written without thinking won't be so ugly.
+ if (!aQuotedText.IsEmpty() && (aQuotedText.Last() != char16_t('\n'))) {
+ quotedStuff.Append(char16_t('\n'));
+ }
+
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, ignoredError);
+ if (NS_WARN_IF(ignoredError.ErrorCodeIs(NS_ERROR_EDITOR_DESTROYED))) {
+ return ignoredError.StealNSResult();
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "TextEditor::OnStartToHandleTopLevelEditSubAction() failed, but ignored");
+
+ // XXX Do we need to support paste-as-quotation in password editor (and
+ // also in single line editor)?
+ MaybeDoAutoPasswordMasking();
+
+ nsresult rv = InsertTextAsSubAction(quotedStuff, SelectionHandling::Delete);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+nsresult TextEditor::SelectEntireDocument() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Element> anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Text> text =
+ Text::FromNodeOrNull(anonymousDivElement->GetFirstChild());
+ MOZ_ASSERT(text);
+
+ MOZ_TRY(SelectionRef().SetStartAndEndInLimiter(
+ *text, 0, *text, text->TextDataLength(), eDirNext,
+ nsISelectionListener::SELECTALL_REASON));
+
+ return NS_OK;
+}
+
+EventTarget* TextEditor::GetDOMEventTarget() const { return mEventTarget; }
+
+void TextEditor::ReinitializeSelection(Element& aElement) {
+ if (NS_WARN_IF(Destroyed())) {
+ return;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return;
+ }
+
+ // We don't need to flush pending notifications here and we don't need to
+ // handle spellcheck at first focus. Therefore, we don't need to call
+ // `TextEditor::OnFocus` here.
+ EditorBase::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();
+}
+
+nsresult TextEditor::OnFocus(const nsINode& aOriginalEventTargetNode) {
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Let's update the layout information right now because there are some
+ // pending notifications and flushing them may cause destroying the editor.
+ presShell->FlushPendingNotifications(FlushType::Layout);
+ if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent(aOriginalEventTargetNode))) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Spell check a textarea the first time that it is focused.
+ nsresult rv = FlushPendingSpellCheck();
+ if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ NS_WARNING("EditorBase::FlushPendingSpellCheck() failed");
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::FlushPendingSpellCheck() failed, but ignored");
+ if (MOZ_UNLIKELY(!CanKeepHandlingFocusEvent(aOriginalEventTargetNode))) {
+ return NS_OK;
+ }
+
+ return EditorBase::OnFocus(aOriginalEventTargetNode);
+}
+
+nsresult TextEditor::OnBlur(const EventTarget* aEventTarget) {
+ nsresult rv = FinalizeSelection();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::FinalizeSelection() failed");
+ return rv;
+}
+
+nsresult TextEditor::SetAttributeOrEquivalent(Element* aElement,
+ nsAtom* aAttribute,
+ const nsAString& aValue,
+ bool aSuppressTransaction) {
+ if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = SetAttributeWithTransaction(*aElement, *aAttribute, aValue);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::RemoveAttributeOrEquivalent(Element* aElement,
+ nsAtom* aAttribute,
+ bool aSuppressTransaction) {
+ if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eRemoveAttribute);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = RemoveAttributeWithTransaction(*aElement, *aAttribute);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::RemoveAttributeWithTransaction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+// static
+void TextEditor::MaskString(nsString& aString, const Text& aTextNode,
+ uint32_t aStartOffsetInString,
+ uint32_t aStartOffsetInText) {
+ MOZ_ASSERT(aTextNode.HasFlag(NS_MAYBE_MASKED));
+ MOZ_ASSERT(aStartOffsetInString == 0 || aStartOffsetInText == 0);
+
+ uint32_t unmaskStart = UINT32_MAX, unmaskLength = 0;
+ TextEditor* textEditor =
+ nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation(&aTextNode);
+ if (textEditor && textEditor->UnmaskedLength() > 0) {
+ unmaskStart = textEditor->UnmaskedStart();
+ unmaskLength = textEditor->UnmaskedLength();
+ // If text is copied from after unmasked range, we can treat this case
+ // as mask all.
+ if (aStartOffsetInText >= unmaskStart + unmaskLength) {
+ unmaskLength = 0;
+ unmaskStart = UINT32_MAX;
+ } else {
+ // If text is copied from middle of unmasked range, reduce the length
+ // and adjust start offset.
+ if (aStartOffsetInText > unmaskStart) {
+ unmaskLength = unmaskStart + unmaskLength - aStartOffsetInText;
+ unmaskStart = 0;
+ }
+ // If text is copied from before start of unmasked range, just adjust
+ // the start offset.
+ else {
+ unmaskStart -= aStartOffsetInText;
+ }
+ // Make the range is in the string.
+ unmaskStart += aStartOffsetInString;
+ }
+ }
+
+ const char16_t kPasswordMask = TextEditor::PasswordMask();
+ for (uint32_t i = aStartOffsetInString; i < aString.Length(); ++i) {
+ bool isSurrogatePair = NS_IS_HIGH_SURROGATE(aString.CharAt(i)) &&
+ i < aString.Length() - 1 &&
+ NS_IS_LOW_SURROGATE(aString.CharAt(i + 1));
+ if (i < unmaskStart || i >= unmaskStart + unmaskLength) {
+ if (isSurrogatePair) {
+ aString.SetCharAt(kPasswordMask, i);
+ aString.SetCharAt(kPasswordMask, i + 1);
+ } else {
+ aString.SetCharAt(kPasswordMask, i);
+ }
+ }
+
+ // Skip the following low surrogate.
+ if (isSurrogatePair) {
+ ++i;
+ }
+ }
+}
+
+nsresult TextEditor::SetUnmaskRangeInternal(uint32_t aStart, uint32_t aLength,
+ uint32_t aTimeout, bool aNotify,
+ bool aForceStartMasking) {
+ if (mPasswordMaskData) {
+ mPasswordMaskData->mIsMaskingPassword = aForceStartMasking || aTimeout != 0;
+
+ // We cannot manage multiple unmasked ranges so that shrink the previous
+ // range first.
+ if (!IsAllMasked()) {
+ mPasswordMaskData->mUnmaskedLength = 0;
+ mPasswordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::No);
+ }
+ }
+
+ // If we're not a password editor, return error since this call does not
+ // make sense.
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData)) {
+ mPasswordMaskData->CancelTimer(PasswordMaskData::ReleaseTimer::Yes);
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ Element* rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ Text* text = Text::FromNodeOrNull(rootElement->GetFirstChild());
+ if (!text || !text->Length()) {
+ // There is no anonymous text node in the editor.
+ return aStart > 0 && aStart != UINT32_MAX ? NS_ERROR_INVALID_ARG : NS_OK;
+ }
+
+ if (aStart < UINT32_MAX) {
+ uint32_t valueLength = text->Length();
+ if (aStart >= valueLength) {
+ return NS_ERROR_INVALID_ARG; // There is no character can be masked.
+ }
+ // If aStart is middle of a surrogate pair, expand it to include the
+ // preceding high surrogate because the caller may want to show a
+ // character before the character at `aStart + 1`.
+ const nsTextFragment* textFragment = text->GetText();
+ if (textFragment->IsLowSurrogateFollowingHighSurrogateAt(aStart)) {
+ mPasswordMaskData->mUnmaskedStart = aStart - 1;
+ // If caller collapses the range, keep it. Otherwise, expand the length.
+ if (aLength > 0) {
+ ++aLength;
+ }
+ } else {
+ mPasswordMaskData->mUnmaskedStart = aStart;
+ }
+ mPasswordMaskData->mUnmaskedLength =
+ std::min(valueLength - UnmaskedStart(), aLength);
+ // If unmasked end is middle of a surrogate pair, expand it to include
+ // the following low surrogate because the caller may want to show a
+ // character after the character at `aStart + aLength`.
+ if (UnmaskedEnd() < valueLength &&
+ textFragment->IsLowSurrogateFollowingHighSurrogateAt(UnmaskedEnd())) {
+ mPasswordMaskData->mUnmaskedLength++;
+ }
+ // If it's first time to mask the unmasking characters with timer, create
+ // the timer now. Then, we'll keep using it for saving the creation cost.
+ if (!HasAutoMaskingTimer() && aLength && aTimeout && UnmaskedLength()) {
+ mPasswordMaskData->mTimer = NS_NewTimer();
+ }
+ } else {
+ if (NS_WARN_IF(aLength != 0)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ mPasswordMaskData->MaskAll();
+ }
+
+ // Notify nsTextFrame of this update if the caller wants this to do it.
+ // Only in this case, script may run.
+ if (aNotify) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ // Notify nsTextFrame of masking range change.
+ if (RefPtr<PresShell> presShell = document->GetObservingPresShell()) {
+ nsAutoScriptBlocker blockRunningScript;
+ uint32_t valueLength = text->Length();
+ CharacterDataChangeInfo changeInfo = {false, 0, valueLength, valueLength,
+ nullptr};
+ presShell->CharacterDataChanged(text, changeInfo);
+ }
+
+ // Scroll caret into the view since masking or unmasking character may
+ // move caret to outside of the view.
+ nsresult rv = ScrollSelectionFocusIntoView();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::ScrollSelectionFocusIntoView() failed");
+ return rv;
+ }
+ }
+
+ if (!IsAllMasked() && aTimeout != 0) {
+ // Initialize the timer to mask the range automatically.
+ MOZ_ASSERT(HasAutoMaskingTimer());
+ DebugOnly<nsresult> rvIgnored = mPasswordMaskData->mTimer->InitWithCallback(
+ this, aTimeout, nsITimer::TYPE_ONE_SHOT);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsITimer::InitWithCallback() failed, but ignored");
+ }
+
+ return NS_OK;
+}
+
+// static
+char16_t TextEditor::PasswordMask() {
+ char16_t ret = LookAndFeel::GetPasswordCharacter();
+ if (!ret) {
+ ret = '*';
+ }
+ return ret;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP TextEditor::Notify(nsITimer* aTimer) {
+ // Check whether our text editor's password flag was changed before this
+ // "hide password character" timer actually fires.
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData)) {
+ return NS_OK;
+ }
+
+ if (IsAllMasked()) {
+ return NS_OK;
+ }
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eHidePassword);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // Mask all characters.
+ nsresult rv = MaskAllCharactersAndNotify();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::MaskAllCharactersAndNotify() failed");
+
+ if (StaticPrefs::editor_password_testing_mask_delay()) {
+ if (RefPtr<Element> target = GetInputEventTargetElement()) {
+ RefPtr<Document> document = target->OwnerDoc();
+ DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchTrustedEvent(
+ document, target, u"MozLastInputMasked"_ns, CanBubble::eYes,
+ Cancelable::eNo);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "nsContentUtils::DispatchTrustedEvent("
+ "MozLastInputMasked) failed, but ignored");
+ }
+ }
+
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+NS_IMETHODIMP TextEditor::GetName(nsACString& aName) {
+ aName.AssignLiteral("TextEditor");
+ return NS_OK;
+}
+
+void TextEditor::WillDeleteText(uint32_t aCurrentLength,
+ uint32_t aRemoveStartOffset,
+ uint32_t aRemoveLength) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData) || IsAllMasked()) {
+ return;
+ }
+
+ // Adjust unmasked range before deletion since DOM mutation may cause
+ // layout referring the range in old text.
+
+ // If we need to mask automatically, mask all now.
+ if (IsMaskingPassword()) {
+ DebugOnly<nsresult> rvIgnored = MaskAllCharacters();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::MaskAllCharacters() failed, but ignored");
+ return;
+ }
+
+ if (aRemoveStartOffset < UnmaskedStart()) {
+ // If removing range is before the unmasked range, move it.
+ if (aRemoveStartOffset + aRemoveLength <= UnmaskedStart()) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(UnmaskedStart() - aRemoveLength, UnmaskedLength());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range starts before unmasked range, and ends in unmasked
+ // range, move and shrink the range.
+ if (aRemoveStartOffset + aRemoveLength < UnmaskedEnd()) {
+ uint32_t unmaskedLengthInRemovingRange =
+ aRemoveStartOffset + aRemoveLength - UnmaskedStart();
+ DebugOnly<nsresult> rvIgnored = SetUnmaskRange(
+ aRemoveStartOffset, UnmaskedLength() - unmaskedLengthInRemovingRange);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range includes all unmasked range, collapse it to the
+ // remove offset.
+ DebugOnly<nsresult> rvIgnored = SetUnmaskRange(aRemoveStartOffset, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ if (aRemoveStartOffset < UnmaskedEnd()) {
+ // If removing range is in unmasked range, shrink the range.
+ if (aRemoveStartOffset + aRemoveLength <= UnmaskedEnd()) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(UnmaskedStart(), UnmaskedLength() - aRemoveLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range starts from unmasked range, and ends after it,
+ // shrink it.
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(UnmaskedStart(), aRemoveStartOffset - UnmaskedStart());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::SetUnmaskRange() failed, but ignored");
+ return;
+ }
+
+ // If removing range is after the unmasked range, keep it.
+}
+
+nsresult TextEditor::DidInsertText(uint32_t aNewLength,
+ uint32_t aInsertedOffset,
+ uint32_t aInsertedLength) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (!IsPasswordEditor() || NS_WARN_IF(!mPasswordMaskData) || IsAllMasked()) {
+ return NS_OK;
+ }
+
+ if (IsMaskingPassword()) {
+ // If we need to mask password, mask all right now.
+ nsresult rv = MaskAllCharactersAndNotify();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::MaskAllCharacters() failed");
+ return rv;
+ }
+
+ if (aInsertedOffset < UnmaskedStart()) {
+ // If insertion point is before unmasked range, expand the unmasked range
+ // to include the new text.
+ nsresult rv = SetUnmaskRangeAndNotify(
+ aInsertedOffset, UnmaskedEnd() + aInsertedLength - aInsertedOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+ }
+
+ if (aInsertedOffset <= UnmaskedEnd()) {
+ // If insertion point is in unmasked range, unmask new text.
+ nsresult rv = SetUnmaskRangeAndNotify(UnmaskedStart(),
+ UnmaskedLength() + aInsertedLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+ }
+
+ // If insertion point is after unmasked range, extend the unmask range to
+ // include the new text.
+ nsresult rv = SetUnmaskRangeAndNotify(
+ UnmaskedStart(), aInsertedOffset + aInsertedLength - UnmaskedStart());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+}
+
+} // namespace mozilla