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.cpp1979
1 files changed, 1979 insertions, 0 deletions
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp
new file mode 100644
index 0000000000..b287a27e88
--- /dev/null
+++ b/editor/libeditor/TextEditor.cpp
@@ -0,0 +1,1979 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TextEditor.h"
+
+#include <algorithm>
+
+#include "EditAggregateTransaction.h"
+#include "HTMLEditUtils.h"
+#include "InternetCiter.h"
+#include "PlaceholderTransaction.h"
+#include "gfxFontUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ContentIterator.h"
+#include "mozilla/EditAction.h"
+#include "mozilla/EditorDOMPoint.h"
+#include "mozilla/HTMLEditor.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/DocumentInlines.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 "nsCopySupport.h"
+#include "nsDebug.h"
+#include "nsDependentSubstring.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIClipboard.h"
+#include "nsIContent.h"
+#include "nsIDocumentEncoder.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 "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 ChildBlockBoundary = HTMLEditUtils::ChildBlockBoundary;
+
+TextEditor::TextEditor()
+ : mMaxTextLength(-1),
+ mUnmaskedStart(UINT32_MAX),
+ mUnmaskedLength(0),
+ mIsMaskingPassword(true) {
+ // 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->mMaskTimer) {
+ tmp->mMaskTimer->Cancel();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaskTimer)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaskTimer)
+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)
+
+nsresult TextEditor::Init(Document& aDoc, Element* aRoot,
+ nsISelectionController* aSelCon, uint32_t aFlags,
+ const nsAString& aInitialValue) {
+ MOZ_ASSERT(!AsHTMLEditor());
+ MOZ_ASSERT(!mInitSucceeded,
+ "TextEditor::Init() called again without calling PreDestroy()?");
+
+ // Init the base editor
+ nsresult rv = EditorBase::Init(aDoc, aRoot, aSelCon, aFlags, aInitialValue);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::Init() failed");
+ return rv;
+ }
+
+ // XXX `eNotEditing` is a lie since InitEditorContentAndSelection() may
+ // insert padding `<br>`.
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ 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 Sholdn'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;
+}
+
+NS_IMETHODIMP TextEditor::SetDocumentCharacterSet(
+ const nsACString& characterSet) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eSetCharacterSet);
+ nsresult rv = editActionData.CanHandleAndMaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "CanHandleAndMaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ rv = EditorBase::SetDocumentCharacterSet(characterSet);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetDocumentCharacterSet() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ // Update META charset element.
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ if (UpdateMetaCharset(*document, characterSet)) {
+ return NS_OK;
+ }
+
+ RefPtr<nsContentList> headElementList =
+ document->GetElementsByTagName(u"head"_ns);
+ if (NS_WARN_IF(!headElementList)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIContent> primaryHeadElement = headElementList->Item(0);
+ if (NS_WARN_IF(!primaryHeadElement)) {
+ return NS_OK;
+ }
+
+ // Create a new meta charset tag
+ RefPtr<Element> metaElement = CreateNodeWithTransaction(
+ *nsGkAtoms::meta, EditorDOMPoint(primaryHeadElement, 0));
+ if (!metaElement) {
+ NS_WARNING(
+ "EditorBase::CreateNodeWithTransaction(nsGkAtoms::meta) failed, but "
+ "ignored");
+ return NS_OK;
+ }
+
+ // Set attributes to the created element
+ if (characterSet.IsEmpty()) {
+ return NS_OK;
+ }
+
+ // not undoable, undo should undo CreateNodeWithTransaction().
+ DebugOnly<nsresult> rvIgnored = NS_OK;
+ rvIgnored = metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
+ u"Content-Type"_ns, true);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::httpEquiv, Content-Type) "
+ "failed, but ignored");
+ rvIgnored = metaElement->SetAttr(
+ kNameSpaceID_None, nsGkAtoms::content,
+ u"text/html;charset="_ns + NS_ConvertASCIItoUTF16(characterSet), true);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "Element::SetAttr(nsGkAtoms::content) failed, but ignored");
+ return NS_OK;
+}
+
+bool TextEditor::UpdateMetaCharset(Document& aDocument,
+ const nsACString& aCharacterSet) {
+ // get a list of META tags
+ RefPtr<nsContentList> metaElementList =
+ aDocument.GetElementsByTagName(u"meta"_ns);
+ if (NS_WARN_IF(!metaElementList)) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < metaElementList->Length(true); ++i) {
+ RefPtr<Element> metaElement = metaElementList->Item(i)->AsElement();
+ MOZ_ASSERT(metaElement);
+
+ nsAutoString currentValue;
+ metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue);
+
+ if (!FindInReadable(u"content-type"_ns, currentValue,
+ nsCaseInsensitiveStringComparator)) {
+ continue;
+ }
+
+ metaElement->GetAttr(kNameSpaceID_None, nsGkAtoms::content, currentValue);
+
+ constexpr auto charsetEquals = u"charset="_ns;
+ nsAString::const_iterator originalStart, start, end;
+ originalStart = currentValue.BeginReading(start);
+ currentValue.EndReading(end);
+ if (!FindInReadable(charsetEquals, start, end,
+ nsCaseInsensitiveStringComparator)) {
+ continue;
+ }
+
+ // set attribute to <original prefix> charset=text/html
+ nsresult rv = SetAttributeWithTransaction(
+ *metaElement, *nsGkAtoms::content,
+ Substring(originalStart, start) + charsetEquals +
+ NS_ConvertASCIItoUTF16(aCharacterSet));
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::SetAttributeWithTransaction(nsGkAtoms::content) failed");
+ return NS_SUCCEEDED(rv);
+ }
+ return false;
+}
+
+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 (IsReadonly()) {
+ // When we're not editable, the events handled on EditorBase.
+ return EditorBase::HandleKeyPressEvent(aKeyboardEvent);
+ }
+
+ if (NS_WARN_IF(!aKeyboardEvent)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ 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:
+ // These keys are handled on EditorBase
+ return EditorBase::HandleKeyPressEvent(aKeyboardEvent);
+ case NS_VK_BACK: {
+ if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() ||
+ aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) {
+ return NS_OK;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ DeleteSelectionAsAction(nsIEditor::ePrevious, nsIEditor::eStrip);
+ aKeyboardEvent->PreventDefault();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::DeleteSelectionAsAction() failed, but ignored");
+ return NS_OK;
+ }
+ case NS_VK_DELETE: {
+ // on certain platforms (such as windows) the shift key
+ // modifies what delete does (cmd_cut in this case).
+ // bailing here to allow the keybindings to do the cut.
+ if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
+ aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() ||
+ aKeyboardEvent->IsOS()) {
+ return NS_OK;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ DeleteSelectionAsAction(nsIEditor::eNext, nsIEditor::eStrip);
+ aKeyboardEvent->PreventDefault();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::DeleteSelectionAsAction() failed, but ignored");
+ return NS_OK;
+ }
+ case NS_VK_TAB: {
+ if (IsTabbable()) {
+ return NS_OK; // let it be used for focus switching
+ }
+
+ if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
+ aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() ||
+ aKeyboardEvent->IsOS()) {
+ return NS_OK;
+ }
+
+ // else we insert the tab straight through
+ aKeyboardEvent->PreventDefault();
+ nsresult rv = OnInputText(u"\t"_ns);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::OnInputText(\\t) 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), "TextEditor::OnInputText() failed");
+ return rv;
+}
+
+nsresult TextEditor::OnInputText(const nsAString& aStringToInsert) {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eInsertText);
+ MOZ_ASSERT(!aStringToInsert.IsVoid());
+ editActionData.SetData(aStringToInsert);
+ // FYI: For conforming to current UI Events spec, we should dispatch
+ // "beforeinput" event before "keypress" event, but here is in a
+ // "keypress" event listener. However, the other browsers dispatch
+ // "beforeinput" event after "keypress" event. Therefore, it makes
+ // sense to follow the other browsers. Spec issue:
+ // https://github.com/w3c/uievents/issues/220
+ 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, *nsGkAtoms::TypingTxnName,
+ ScrollSelectionIntoView::Yes);
+ rv = InsertTextAsSubAction(aStringToInsert);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() 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);
+ 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);
+ MOZ_ASSERT(!AsHTMLEditor());
+
+ 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);
+ rv = SetTextAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::ReplaceTextAsAction(
+ const nsAString& aString, nsRange* aReplaceRange,
+ AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
+
+ AutoEditActionDataSetter editActionData(*this, EditAction::eReplaceText,
+ aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ if (aAllowBeforeInputEventCancelable == AllowBeforeInputEventCancelable::No) {
+ editActionData.MakeBeforeInputEventNonCancelable();
+ }
+
+ if (!AsHTMLEditor()) {
+ editActionData.SetData(aString);
+ } else {
+ editActionData.InitializeDataTransfer(aString);
+ RefPtr<StaticRange> targetRange;
+ if (aReplaceRange) {
+ // Compute offset of the range before dispatching `beforeinput` event
+ // because it may be referred after the DOM tree is changed and the
+ // range may have not computed the offset yet.
+ targetRange = StaticRange::Create(
+ aReplaceRange->GetStartContainer(), aReplaceRange->StartOffset(),
+ aReplaceRange->GetEndContainer(), aReplaceRange->EndOffset(),
+ IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "StaticRange::Create() failed");
+ } else {
+ Element* editingHost = AsHTMLEditor()->GetActiveEditingHost();
+ NS_WARNING_ASSERTION(editingHost,
+ "No active editing host, no target ranges");
+ if (editingHost) {
+ targetRange = StaticRange::Create(
+ editingHost, 0, editingHost, editingHost->Length(), IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "StaticRange::Create() failed");
+ }
+ }
+ if (targetRange && targetRange->IsPositioned()) {
+ editActionData.AppendTargetRange(*targetRange);
+ }
+ }
+
+ nsresult 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);
+
+ // This should emulates inserting text for better undo/redo behavior.
+ IgnoredErrorResult ignoredError;
+ AutoEditSubActionNotifier startToHandleEditSubAction(
+ *this, EditSubAction::eInsertText, nsIEditor::eNext, 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");
+
+ if (!aReplaceRange) {
+ nsresult rv = SetTextAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetTextAsSubAction() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (aString.IsEmpty() && aReplaceRange->Collapsed()) {
+ NS_WARNING("Setting value was empty and replaced range was empty");
+ 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);
+
+ // Select the range but as far as possible, we should not create new range
+ // even if it's part of special Selection.
+ ErrorResult error;
+ SelectionRefPtr()->RemoveAllRanges(error);
+ if (error.Failed()) {
+ NS_WARNING("Selection::RemoveAllRanges() failed");
+ return error.StealNSResult();
+ }
+ MOZ_KnownLive(SelectionRefPtr())
+ ->AddRangeAndSelectFramesAndNotifyListeners(*aReplaceRange, error);
+ if (error.Failed()) {
+ NS_WARNING("Selection::AddRangeAndSelectFramesAndNotifyListeners() failed");
+ return error.StealNSResult();
+ }
+
+ rv = ReplaceSelectionAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::ReplaceSelectionAsSubAction() 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 (IsPlaintextEditor() && !IsIMEComposing() && !IsUndoRedoEnabled() &&
+ GetEditAction() != EditAction::eReplaceText && mMaxTextLength < 0) {
+ EditActionResult result = SetTextWithoutTransaction(aString);
+ if (result.Failed() || result.Canceled() || result.Handled()) {
+ NS_WARNING_ASSERTION(result.Succeeded(),
+ "TextEditor::SetTextWithoutTransaction() failed");
+ return result.Rv();
+ }
+ }
+
+ {
+ // 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);
+
+ RefPtr<Element> rootElement = GetRoot();
+ if (NS_WARN_IF(!rootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // We want to select trailing `<br>` element to remove all nodes to replace
+ // all, but TextEditor::SelectEntireDocument() doesn't select such `<br>`
+ // elements.
+ // XXX We should make ReplaceSelectionAsSubAction() take range. Then,
+ // we can saving the expensive cost of modifying `Selection` here.
+ nsresult rv;
+ if (IsEmpty()) {
+ rv = MOZ_KnownLive(SelectionRefPtr())->CollapseInLimiter(rootElement, 0);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Selection::CollapseInLimiter() failed, but ignored");
+ } else {
+ // XXX Oh, we shouldn't select padding `<br>` element for empty last
+ // line here since we will need to recreate it in multiline
+ // text editor.
+ ErrorResult error;
+ SelectionRefPtr()->SelectAllChildren(*rootElement, error);
+ NS_WARNING_ASSERTION(
+ !error.Failed(),
+ "Selection::SelectAllChildren() failed, but ignored");
+ rv = error.StealNSResult();
+ }
+ if (NS_SUCCEEDED(rv)) {
+ DebugOnly<nsresult> rvIgnored = ReplaceSelectionAsSubAction(aString);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "TextEditor::ReplaceSelectionAsSubAction() failed, but ignored");
+ }
+ }
+
+ // Destroying AutoUpdateViewBatch may cause destroying us.
+ return NS_WARN_IF(Destroyed()) ? NS_ERROR_EDITOR_DESTROYED : NS_OK;
+}
+
+nsresult TextEditor::ReplaceSelectionAsSubAction(const nsAString& aString) {
+ // TODO: Move this method to `EditorBase`.
+ if (aString.IsEmpty()) {
+ nsresult rv = DeleteSelectionAsSubAction(
+ nsIEditor::eNone,
+ IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsSubAction(eNone) failed");
+ return rv;
+ }
+
+ nsresult rv = InsertTextAsSubAction(aString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+bool TextEditor::EnsureComposition(WidgetCompositionEvent& aCompositionEvent) {
+ if (mComposition) {
+ return true;
+ }
+ // The compositionstart event must cause creating new TextComposition
+ // instance at being dispatched by IMEStateManager.
+ mComposition = IMEStateManager::GetTextCompositionFor(&aCompositionEvent);
+ if (!mComposition) {
+ // However, TextComposition may be committed before the composition
+ // event comes here.
+ return false;
+ }
+ mComposition->StartHandlingComposition(this);
+ return true;
+}
+
+nsresult TextEditor::OnCompositionStart(
+ WidgetCompositionEvent& aCompositionStartEvent) {
+ if (mComposition) {
+ NS_WARNING("There was a composition at receiving compositionstart event");
+ return NS_OK;
+ }
+
+ // "beforeinput" event shouldn't be fired before "compositionstart".
+ AutoEditActionDataSetter editActionData(*this, EditAction::eStartComposition);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ EnsureComposition(aCompositionStartEvent);
+ NS_WARNING_ASSERTION(mComposition, "Failed to get TextComposition instance?");
+ return NS_OK;
+}
+
+nsresult TextEditor::OnCompositionChange(
+ WidgetCompositionEvent& aCompositionChangeEvent) {
+ MOZ_ASSERT(aCompositionChangeEvent.mMessage == eCompositionChange,
+ "The event should be eCompositionChange");
+
+ if (!mComposition) {
+ NS_WARNING(
+ "There is no composition, but receiving compositionchange event");
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoEditActionDataSetter editActionData(*this,
+ EditAction::eUpdateComposition);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If:
+ // - new composition string is not empty,
+ // - there is no composition string in the DOM tree,
+ // - and there is non-collapsed Selection,
+ // the selected content will be removed by this composition.
+ if (aCompositionChangeEvent.mData.IsEmpty() &&
+ mComposition->String().IsEmpty() && !SelectionRefPtr()->IsCollapsed()) {
+ editActionData.UpdateEditAction(EditAction::eDeleteByComposition);
+ }
+
+ // If Input Events Level 2 is enabled, EditAction::eDeleteByComposition is
+ // mapped to EditorInputType::eDeleteByComposition and it requires null
+ // for InputEvent.data. Therefore, only otherwise, we should set data.
+ if (ToInputType(editActionData.GetEditAction()) !=
+ EditorInputType::eDeleteByComposition) {
+ MOZ_ASSERT(ToInputType(editActionData.GetEditAction()) ==
+ EditorInputType::eInsertCompositionText);
+ MOZ_ASSERT(!aCompositionChangeEvent.mData.IsVoid());
+ editActionData.SetData(aCompositionChangeEvent.mData);
+ }
+
+ // If we're an `HTMLEditor` and this is second or later composition change,
+ // we should set target range to the range of composition string.
+ // Otherwise, set target ranges to selection ranges (will be done by
+ // editActionData itself before dispatching `beforeinput` event).
+ if (AsHTMLEditor() && mComposition->GetContainerTextNode()) {
+ RefPtr<StaticRange> targetRange = StaticRange::Create(
+ mComposition->GetContainerTextNode(),
+ mComposition->XPOffsetInTextNode(),
+ mComposition->GetContainerTextNode(),
+ mComposition->XPEndOffsetInTextNode(), IgnoreErrors());
+ NS_WARNING_ASSERTION(targetRange && targetRange->IsPositioned(),
+ "StaticRange::Create() failed");
+ if (targetRange && targetRange->IsPositioned()) {
+ editActionData.AppendTargetRange(*targetRange);
+ }
+ }
+
+ // TODO: We need to use different EditAction value for beforeinput event
+ // if the event is followed by "compositionend" because corresponding
+ // "input" event will be fired from OnCompositionEnd() later with
+ // different EditAction value.
+ // TODO: If Input Events Level 2 is enabled, "beforeinput" event may be
+ // actually canceled if edit action is eDeleteByComposition. In such
+ // case, we might need to keep selected text, but insert composition
+ // string before or after the selection. However, the spec is still
+ // unstable. We should keep handling the composition since other
+ // parts including widget may not be ready for such complicated
+ // behavior.
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (rv != NS_ERROR_EDITOR_ACTION_CANCELED && NS_FAILED(rv)) {
+ NS_WARNING("MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+
+ if (!EnsureComposition(aCompositionChangeEvent)) {
+ NS_WARNING("TextEditor::EnsureComposition() failed");
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!GetPresShell())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // NOTE: TextComposition should receive selection change notification before
+ // CompositionChangeEventHandlingMarker notifies TextComposition of the
+ // end of handling compositionchange event because TextComposition may
+ // need to ignore selection changes caused by composition. Therefore,
+ // CompositionChangeEventHandlingMarker must be destroyed after a call
+ // of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or
+ // NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies
+ // TextComposition of a selection change.
+ MOZ_ASSERT(
+ !mPlaceholderBatch,
+ "UpdateIMEComposition() must be called without place holder batch");
+ TextComposition::CompositionChangeEventHandlingMarker
+ compositionChangeEventHandlingMarker(mComposition,
+ &aCompositionChangeEvent);
+
+ RefPtr<nsCaret> caret = GetCaret();
+
+ {
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::IMETxnName,
+ ScrollSelectionIntoView::Yes);
+
+ MOZ_ASSERT(
+ mIsInEditSubAction,
+ "AutoPlaceholderBatch should've notified the observes of before-edit");
+ nsString data(aCompositionChangeEvent.mData);
+ if (!AsTextEditor()) {
+ nsContentUtils::PlatformToDOMLineBreaks(data);
+ }
+ rv = InsertTextAsSubAction(data);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+
+ if (caret) {
+ caret->SetSelection(SelectionRefPtr());
+ }
+ }
+
+ // If still composing, we should fire input event via observer.
+ // Note that if the composition will be committed by the following
+ // compositionend event, we don't need to notify editor observes of this
+ // change.
+ // NOTE: We must notify after the auto batch will be gone.
+ if (!aCompositionChangeEvent.IsFollowedByCompositionEnd()) {
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ }
+
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+void TextEditor::OnCompositionEnd(
+ WidgetCompositionEvent& aCompositionEndEvent) {
+ if (!mComposition) {
+ NS_WARNING("There is no composition, but receiving compositionend event");
+ return;
+ }
+
+ EditAction editAction = aCompositionEndEvent.mData.IsEmpty()
+ ? EditAction::eCancelComposition
+ : EditAction::eCommitComposition;
+ AutoEditActionDataSetter editActionData(*this, editAction);
+ // If Input Events Level 2 is enabled, EditAction::eCancelComposition is
+ // mapped to EditorInputType::eDeleteCompositionText and it requires null
+ // for InputEvent.data. Therefore, only otherwise, we should set data.
+ if (ToInputType(editAction) != EditorInputType::eDeleteCompositionText) {
+ MOZ_ASSERT(
+ ToInputType(editAction) == EditorInputType::eInsertCompositionText ||
+ ToInputType(editAction) == EditorInputType::eInsertFromComposition);
+ MOZ_ASSERT(!aCompositionEndEvent.mData.IsVoid());
+ editActionData.SetData(aCompositionEndEvent.mData);
+ }
+
+ // commit the IME transaction..we can get at it via the transaction mgr.
+ // Note that this means IME won't work without an undo stack!
+ if (mTransactionManager) {
+ if (nsCOMPtr<nsITransaction> transaction =
+ mTransactionManager->PeekUndoStack()) {
+ if (RefPtr<EditTransactionBase> transactionBase =
+ transaction->GetAsEditTransactionBase()) {
+ if (PlaceholderTransaction* placeholderTransaction =
+ transactionBase->GetAsPlaceholderTransaction()) {
+ placeholderTransaction->Commit();
+ }
+ }
+ }
+ }
+
+ // Note that this just marks as that we've already handled "beforeinput" for
+ // preventing assertions in FireInputEvent(). Note that corresponding
+ // "beforeinput" event for the following "input" event should've already
+ // been dispatched from `OnCompositionChange()`.
+ DebugOnly<nsresult> rvIgnored =
+ editActionData.MaybeDispatchBeforeInputEvent();
+ MOZ_ASSERT(rvIgnored != NS_ERROR_EDITOR_ACTION_CANCELED,
+ "Why beforeinput event was canceled in this case?");
+ MOZ_ASSERT(NS_SUCCEEDED(rvIgnored),
+ "MaybeDispatchBeforeInputEvent() should just mark the instance as "
+ "handled it");
+
+ // Composition string may have hidden the caret. Therefore, we need to
+ // cancel it here.
+ HideCaret(false);
+
+ // FYI: mComposition still keeps storing container text node of committed
+ // string, its offset and length. However, they will be invalidated
+ // soon since its Destroy() will be called by IMEStateManager.
+ mComposition->EndHandlingComposition(this);
+ mComposition = nullptr;
+
+ // notify editor observers of action
+ // FYI: With current draft, "input" event should be fired from
+ // OnCompositionChange(), however, it requires a lot of our UI code
+ // change and does not make sense. See spec issue:
+ // https://github.com/w3c/uievents/issues/202
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+}
+
+already_AddRefed<Element> TextEditor::GetInputEventTargetElement() const {
+ nsCOMPtr<Element> target = do_QueryInterface(mEventTarget);
+ return target.forget();
+}
+
+bool TextEditor::IsEmpty() const {
+ if (mPaddingBRElementForEmptyEditor) {
+ return true;
+ }
+
+ // 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
+ }
+
+ // Only when there is non-empty text node, we are not empty.
+ return !anonymousDivElement->GetFirstChild() ||
+ !anonymousDivElement->GetFirstChild()->IsText() ||
+ !anonymousDivElement->GetFirstChild()->Length();
+}
+
+NS_IMETHODIMP TextEditor::GetDocumentIsEmpty(bool* aDocumentIsEmpty) {
+ MOZ_ASSERT(aDocumentIsEmpty);
+ *aDocumentIsEmpty = IsEmpty();
+ return NS_OK;
+}
+
+NS_IMETHODIMP TextEditor::GetTextLength(int32_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;
+}
+
+nsresult TextEditor::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);
+
+ 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 (NS_WARN_IF(!mRootElement)) {
+ NS_WARNING("Failed to handle padding BR Element due to no root element");
+ rv = NS_ERROR_FAILURE;
+ } else {
+ // The idea here is to see if the magic empty node has suddenly
+ // reappeared as the result of the undo. If it has, set our state
+ // so we remember it. There is a tradeoff between doing here and
+ // at redo, or doing it everywhere else that might care. Since undo
+ // and redo are relatively rare, it makes sense to take the (small)
+ // performance hit here.
+ nsIContent* firstLeafChild = HTMLEditUtils::GetFirstLeafChild(
+ *mRootElement, ChildBlockBoundary::Ignore);
+ if (firstLeafChild &&
+ EditorUtils::IsPaddingBRElementForEmptyEditor(*firstLeafChild)) {
+ mPaddingBRElementForEmptyEditor =
+ static_cast<HTMLBRElement*>(firstLeafChild);
+ } else {
+ mPaddingBRElementForEmptyEditor = nullptr;
+ }
+ }
+ }
+
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+nsresult TextEditor::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);
+
+ 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 (NS_WARN_IF(!mRootElement)) {
+ NS_WARNING("Failed to handle padding BR element due to no root element");
+ rv = NS_ERROR_FAILURE;
+ } else {
+ // We may take empty <br> element for empty editor back with this redo.
+ // We need to store it again.
+ // XXX Looks like that this is too slow if there are a lot of nodes.
+ // Shouldn't we just scan children in the root?
+ nsCOMPtr<nsIHTMLCollection> nodeList =
+ mRootElement->GetElementsByTagName(u"br"_ns);
+ MOZ_ASSERT(nodeList);
+ Element* brElement =
+ nodeList->Length() == 1 ? nodeList->Item(0) : nullptr;
+ if (brElement &&
+ EditorUtils::IsPaddingBRElementForEmptyEditor(*brElement)) {
+ mPaddingBRElementForEmptyEditor =
+ static_cast<HTMLBRElement*>(brElement);
+ } else {
+ mPaddingBRElementForEmptyEditor = nullptr;
+ }
+ }
+ }
+
+ NotifyEditorObservers(eNotifyEditorObserversOfEnd);
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+bool TextEditor::IsCopyToClipboardAllowedInternal() const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ if (SelectionRefPtr()->IsCollapsed()) {
+ return false;
+ }
+
+ if (!IsSingleLineEditor() || !IsPasswordEditor()) {
+ 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() || mUnmaskedLength == 0) {
+ 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 (SelectionRefPtr()->RangeCount() > 1) {
+ return false;
+ }
+
+ uint32_t selectionStart = 0, selectionEnd = 0;
+ nsContentUtils::GetSelectionInTextControl(SelectionRefPtr(), mRootElement,
+ selectionStart, selectionEnd);
+ return mUnmaskedStart <= selectionStart && UnmaskedEnd() >= selectionEnd;
+}
+
+bool TextEditor::FireClipboardEvent(EventMessage aEventMessage,
+ int32_t aSelectionType,
+ bool* aActionTaken) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ if (aEventMessage == ePaste) {
+ CommitComposition();
+ }
+
+ RefPtr<PresShell> presShell = GetPresShell();
+ if (NS_WARN_IF(!presShell)) {
+ return false;
+ }
+
+ RefPtr<Selection> sel = SelectionRefPtr();
+ if (IsHTMLEditor() && aEventMessage == eCopy && sel->IsCollapsed()) {
+ // If we don't have a usable selection for copy and we're an HTML editor
+ // (which is global for the document) try to use the last focused selection
+ // instead.
+ sel = nsCopySupport::GetSelectionForCopy(GetDocument());
+ }
+
+ const bool clipboardEventCanceled = !nsCopySupport::FireClipboardEvent(
+ aEventMessage, aSelectionType, presShell, sel, aActionTaken);
+ NotifyOfDispatchingClipboardEvent();
+
+ // If the event handler caused the editor to be destroyed, return false.
+ // Otherwise return true if the event was not cancelled.
+ return !clipboardEventCanceled && !mDidPreDestroy;
+}
+
+nsresult TextEditor::CutAsAction(nsIPrincipal* aPrincipal) {
+ // TODO: Move this method to `EditorBase`.
+ AutoEditActionDataSetter editActionData(*this, EditAction::eCut, aPrincipal);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ bool actionTaken = false;
+ if (!FireClipboardEvent(eCut, nsIClipboard::kGlobalClipboard, &actionTaken)) {
+ return EditorBase::ToGenericNSResult(
+ actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED);
+ }
+
+ // Dispatch "beforeinput" event after dispatching "cut" event.
+ nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
+ if (NS_FAILED(rv)) {
+ NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
+ "MaybeDispatchBeforeInputEvent() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ // XXX This transaction name is referred by PlaceholderTransaction::Merge()
+ // so that we need to keep using it here.
+ AutoPlaceholderBatch treatAsOneTransaction(*this, *nsGkAtoms::DeleteTxnName,
+ ScrollSelectionIntoView::Yes);
+ rv = DeleteSelectionAsSubAction(
+ eNone, IsTextEditor() ? nsIEditor::eNoStrip : nsIEditor::eStrip);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionAsSubAction(eNone) failed, but ignored");
+ return EditorBase::ToGenericNSResult(rv);
+}
+
+bool TextEditor::AreClipboardCommandsUnconditionallyEnabled() const {
+ Document* document = GetDocument();
+ return document && document->AreClipboardCommandsUnconditionallyEnabled();
+}
+
+bool TextEditor::IsCutCommandEnabled() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+
+ if (AreClipboardCommandsUnconditionallyEnabled()) {
+ return true;
+ }
+
+ return IsModifiable() && IsCopyToClipboardAllowedInternal();
+}
+
+NS_IMETHODIMP TextEditor::Copy() {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eCopy);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ bool actionTaken = false;
+ FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken);
+
+ return EditorBase::ToGenericNSResult(
+ actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED);
+}
+
+bool TextEditor::IsCopyCommandEnabled() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+
+ if (AreClipboardCommandsUnconditionallyEnabled()) {
+ return true;
+ }
+
+ return IsCopyToClipboardAllowedInternal();
+}
+
+bool TextEditor::CanDeleteSelection() const {
+ AutoEditActionDataSetter editActionData(*this, EditAction::eNotEditing);
+ if (NS_WARN_IF(!editActionData.CanHandle())) {
+ return false;
+ }
+
+ return IsModifiable() && !SelectionRefPtr()->IsCollapsed();
+}
+
+already_AddRefed<nsIDocumentEncoder> TextEditor::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(SelectionRefPtr()))) {
+ 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 {
+ dom::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();
+}
+
+NS_IMETHODIMP TextEditor::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),
+ "TextEditor::ComputeValueInternal() failed");
+ // This is low level API for XUL application. So, we should return raw
+ // error code here.
+ return rv;
+}
+
+nsresult TextEditor::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")) {
+ // 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?
+ if (!(aDocumentEncoderFlags & (nsIDocumentEncoder::OutputSelectionOnly |
+ nsIDocumentEncoder::OutputWrap))) {
+ EditActionResult result =
+ ComputeValueFromTextNodeAndPaddingBRElement(aOutputString);
+ if (result.Failed() || result.Canceled() || result.Handled()) {
+ NS_WARNING_ASSERTION(
+ result.Succeeded(),
+ "TextEditor::ComputeValueFromTextNodeAndPaddingBRElement() failed");
+ return result.Rv();
+ }
+ }
+ }
+
+ nsAutoCString charset;
+ nsresult rv = GetDocumentCharsetInternal(charset);
+ if (NS_FAILED(rv) || charset.IsEmpty()) {
+ charset.AssignLiteral("windows-1252");
+ }
+
+ nsCOMPtr<nsIDocumentEncoder> encoder =
+ GetAndInitDocEncoder(aFormatType, aDocumentEncoderFlags, charset);
+ if (!encoder) {
+ NS_WARNING("TextEditor::GetAndInitDocEncoder() failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = encoder->EncodeToString(aOutputString);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "nsIDocumentEncoder::EncodeToString() failed");
+ return rv;
+}
+
+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;
+ }
+
+ // 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
+ nsCOMPtr<nsITransferable> trans;
+ rv = PrepareTransferable(getter_AddRefs(trans));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::PrepareTransferable() failed");
+ return EditorBase::ToGenericNSResult(rv);
+ }
+ if (!trans) {
+ 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);
+ 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;
+ nsresult rv = InternetCiter::GetCiteString(aQuotedText, quotedStuff);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("InternetCiter::GetCiteString() failed");
+ return rv;
+ }
+
+ // 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();
+
+ rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ rv = InsertTextAsSubAction(quotedStuff);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "EditorBase::InsertTextAsSubAction() failed");
+ return rv;
+}
+
+nsresult TextEditor::SharedOutputString(uint32_t aFlags, bool* aIsCollapsed,
+ nsAString& aResult) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ *aIsCollapsed = SelectionRefPtr()->IsCollapsed();
+
+ if (!*aIsCollapsed) {
+ aFlags |= nsIDocumentEncoder::OutputSelectionOnly;
+ }
+ // If the selection isn't collapsed, we'll use the whole document.
+ nsresult rv = ComputeValueInternal(u"text/plain"_ns, aFlags, aResult);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::ComputeValueInternal(text/plain) failed");
+ return rv;
+}
+
+nsresult TextEditor::SelectEntireDocument() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!AsHTMLEditor());
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ RefPtr<Element> anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ // If we're empty, don't select all children because that would select the
+ // padding <br> element for empty editor.
+ if (IsEmpty()) {
+ nsresult rv = MOZ_KnownLive(SelectionRefPtr())
+ ->CollapseInLimiter(anonymousDivElement, 0);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Selection::CollapseInLimiter() failed");
+ return rv;
+ }
+
+ // XXX We just need to select all of first text node (if there is).
+ // Why do we do this kind of complicated things?
+
+ // Don't select the trailing BR node if we have one
+ nsCOMPtr<nsIContent> childNode;
+ nsresult rv = EditorBase::GetEndChildNode(*SelectionRefPtr(),
+ getter_AddRefs(childNode));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::GetEndChildNode() failed");
+ return rv;
+ }
+ if (childNode) {
+ childNode = childNode->GetPreviousSibling();
+ }
+
+ if (childNode &&
+ EditorUtils::IsPaddingBRElementForEmptyLastLine(*childNode)) {
+ ErrorResult error;
+ MOZ_KnownLive(SelectionRefPtr())
+ ->SetStartAndEndInLimiter(RawRangeBoundary(anonymousDivElement, 0u),
+ EditorRawDOMPoint(childNode), error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SetStartAndEndInLimiter() failed");
+ return error.StealNSResult();
+ }
+
+ ErrorResult error;
+ SelectionRefPtr()->SelectAllChildren(*anonymousDivElement, error);
+ NS_WARNING_ASSERTION(!error.Failed(),
+ "Selection::SelectAllChildren() failed");
+ return error.StealNSResult();
+}
+
+EventTarget* TextEditor::GetDOMEventTarget() const { return mEventTarget; }
+
+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);
+}
+
+nsresult TextEditor::EnsurePaddingBRElementForEmptyEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!AsHTMLEditor());
+
+ // If there is padding <br> element for empty editor, we have no work to do.
+ if (mPaddingBRElementForEmptyEditor) {
+ return NS_OK;
+ }
+
+ // Likewise, nothing to be done if we could never have inserted a trailing
+ // <br> element.
+ // XXX Why don't we use same path for <textarea> and <input>?
+ if (IsSingleLineEditor()) {
+ nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ if (NS_WARN_IF(!mRootElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t childCount = mRootElement->GetChildCount();
+ if (childCount == 0) {
+ nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ if (childCount > 1) {
+ return NS_OK;
+ }
+
+ RefPtr<HTMLBRElement> brElement =
+ HTMLBRElement::FromNodeOrNull(mRootElement->GetFirstChild());
+ if (!brElement ||
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(*brElement)) {
+ return NS_OK;
+ }
+
+ // Rather than deleting this node from the DOM tree we should instead
+ // morph this <br> element into the padding <br> element for editor.
+ mPaddingBRElementForEmptyEditor = std::move(brElement);
+ mPaddingBRElementForEmptyEditor->UnsetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
+ mPaddingBRElementForEmptyEditor->SetFlags(NS_PADDING_FOR_EMPTY_EDITOR);
+
+ return NS_OK;
+}
+
+nsresult TextEditor::SetUnmaskRangeInternal(uint32_t aStart, uint32_t aLength,
+ uint32_t aTimeout, bool aNotify,
+ bool aForceStartMasking) {
+ mIsMaskingPassword = aForceStartMasking || aTimeout != 0;
+
+ // We cannot manage multiple unmasked ranges so that shrink the previous
+ // range first.
+ if (!IsAllMasked()) {
+ mUnmaskedLength = 0;
+ if (mMaskTimer) {
+ mMaskTimer->Cancel();
+ }
+ }
+
+ // If we're not a password editor, return error since this call does not
+ // make sense.
+ if (!IsPasswordEditor()) {
+ if (mMaskTimer) {
+ mMaskTimer = nullptr;
+ }
+ 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) {
+ // 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)) {
+ mUnmaskedStart = aStart - 1;
+ // If caller collapses the range, keep it. Otherwise, expand the length.
+ if (aLength > 0) {
+ ++aLength;
+ }
+ } else {
+ mUnmaskedStart = aStart;
+ }
+ mUnmaskedLength = std::min(valueLength - mUnmaskedStart, 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())) {
+ ++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 (!mMaskTimer && aLength && aTimeout && mUnmaskedLength) {
+ mMaskTimer = NS_NewTimer();
+ }
+ } else {
+ if (NS_WARN_IF(aLength != 0)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ mUnmaskedStart = UINT32_MAX;
+ mUnmaskedLength = 0;
+ }
+
+ // 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(mMaskTimer);
+ DebugOnly<nsresult> rvIgnored =
+ mMaskTimer->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()) {
+ 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() || 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 (mIsMaskingPassword) {
+ DebugOnly<nsresult> rvIgnored = MaskAllCharacters();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
+ "TextEditor::MaskAllCharacters() failed, but ignored");
+ return;
+ }
+
+ if (aRemoveStartOffset < mUnmaskedStart) {
+ // If removing range is before the unmasked range, move it.
+ if (aRemoveStartOffset + aRemoveLength <= mUnmaskedStart) {
+ DebugOnly<nsresult> rvIgnored =
+ SetUnmaskRange(mUnmaskedStart - aRemoveLength, mUnmaskedLength);
+ 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 - mUnmaskedStart;
+ DebugOnly<nsresult> rvIgnored = SetUnmaskRange(
+ aRemoveStartOffset, mUnmaskedLength - 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(mUnmaskedStart, mUnmaskedLength - 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(mUnmaskedStart, aRemoveStartOffset - mUnmaskedStart);
+ 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() || IsAllMasked()) {
+ return NS_OK;
+ }
+
+ if (mIsMaskingPassword) {
+ // 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 < mUnmaskedStart) {
+ // 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(mUnmaskedStart,
+ mUnmaskedLength + 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(
+ mUnmaskedStart, aInsertedOffset + aInsertedLength - mUnmaskedStart);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return rv;
+}
+
+} // namespace mozilla