summaryrefslogtreecommitdiffstats
path: root/editor/libeditor/TextEditSubActionHandler.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /editor/libeditor/TextEditSubActionHandler.cpp
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor/TextEditSubActionHandler.cpp')
-rw-r--r--editor/libeditor/TextEditSubActionHandler.cpp1043
1 files changed, 1043 insertions, 0 deletions
diff --git a/editor/libeditor/TextEditSubActionHandler.cpp b/editor/libeditor/TextEditSubActionHandler.cpp
new file mode 100644
index 0000000000..bc6ce9894f
--- /dev/null
+++ b/editor/libeditor/TextEditSubActionHandler.cpp
@@ -0,0 +1,1043 @@
+/* -*- 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 "mozilla/Assertions.h"
+#include "mozilla/EditAction.h"
+#include "mozilla/EditorDOMPoint.h"
+#include "mozilla/EditorUtils.h"
+#include "mozilla/HTMLEditor.h"
+#include "mozilla/LookAndFeel.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_editor.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/HTMLBRElement.h"
+#include "mozilla/dom/NodeFilterBinding.h"
+#include "mozilla/dom/NodeIterator.h"
+#include "mozilla/dom/Selection.h"
+#include "nsAString.h"
+#include "nsCOMPtr.h"
+#include "nsCRT.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIHTMLCollection.h"
+#include "nsINode.h"
+#include "nsISupportsBase.h"
+#include "nsLiteralString.h"
+#include "nsNameSpaceManager.h"
+#include "nsPrintfCString.h"
+#include "nsTextNode.h"
+#include "nsUnicharUtils.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+#define CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY \
+ if (IsReadonly()) { \
+ return EditActionCanceled(NS_OK); \
+ }
+
+nsresult TextEditor::InitEditorContentAndSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ nsresult rv = MaybeCreatePaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() failed");
+ return rv;
+ }
+
+ // If the selection hasn't been set up yet, set it up collapsed to the end of
+ // our editable content.
+ // XXX I think that this shouldn't do it in `HTMLEditor` because it maybe
+ // removed by the web app and if they call `Selection::AddRange()`,
+ // it may cause multiple selection ranges.
+ if (!SelectionRefPtr()->RangeCount()) {
+ nsresult rv = CollapseSelectionToEnd();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::CollapseSelectionToEnd() failed");
+ return rv;
+ }
+ }
+
+ if (IsPlaintextEditor() && !IsSingleLineEditor()) {
+ nsresult rv = EnsurePaddingBRElementInMultilineEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "TextEditor::EnsurePaddingBRElementInMultilineEditor() failed");
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+void TextEditor::OnStartToHandleTopLevelEditSubAction(
+ EditSubAction aTopLevelEditSubAction,
+ nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!AsHTMLEditor());
+ MOZ_ASSERT(!aRv.Failed());
+
+ EditorBase::OnStartToHandleTopLevelEditSubAction(
+ aTopLevelEditSubAction, aDirectionOfTopLevelEditSubAction, aRv);
+
+ MOZ_ASSERT(GetTopLevelEditSubAction() == aTopLevelEditSubAction);
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() ==
+ aDirectionOfTopLevelEditSubAction);
+
+ if (NS_WARN_IF(Destroyed())) {
+ aRv.Throw(NS_ERROR_EDITOR_DESTROYED);
+ return;
+ }
+
+ if (NS_WARN_IF(!mInitSucceeded)) {
+ return;
+ }
+
+ if (aTopLevelEditSubAction == EditSubAction::eSetText) {
+ // SetText replaces all text, so spell checker handles starting from the
+ // start of new value.
+ SetSpellCheckRestartPoint(EditorDOMPoint(mRootElement, 0));
+ return;
+ }
+
+ if (aTopLevelEditSubAction == EditSubAction::eInsertText ||
+ aTopLevelEditSubAction == EditSubAction::eInsertTextComingFromIME) {
+ // For spell checker, previous selected node should be text node if
+ // possible. If anchor is root of editor, it may become invalid offset
+ // after inserting text.
+ EditorRawDOMPoint point = FindBetterInsertionPoint(
+ EditorRawDOMPoint(SelectionRefPtr()->AnchorRef()));
+ NS_WARNING_ASSERTION(
+ point.IsSet(),
+ "EditorBase::FindBetterInsertionPoint() failed, but ignored");
+ if (point.IsSet()) {
+ SetSpellCheckRestartPoint(point);
+ return;
+ }
+ }
+ if (SelectionRefPtr()->AnchorRef().IsSet()) {
+ SetSpellCheckRestartPoint(
+ EditorRawDOMPoint(SelectionRefPtr()->AnchorRef()));
+ }
+}
+
+nsresult TextEditor::OnEndHandlingTopLevelEditSubAction() {
+ MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable());
+ MOZ_ASSERT(!AsHTMLEditor());
+
+ nsresult rv;
+ while (true) {
+ if (NS_WARN_IF(Destroyed())) {
+ rv = NS_ERROR_EDITOR_DESTROYED;
+ break;
+ }
+
+ // XXX Probably, we should spellcheck again after edit action (not top-level
+ // sub-action) is handled because the ranges can be referred only by
+ // users.
+ if (NS_FAILED(rv = HandleInlineSpellCheckAfterEdit())) {
+ NS_WARNING("TextEditor::HandleInlineSpellCheckAfterEdit() failed");
+ break;
+ }
+
+ if (NS_FAILED(rv = EnsurePaddingBRElementForEmptyEditor())) {
+ NS_WARNING("TextEditor::EnsurePaddingBRElementForEmptyEditor() failed");
+ break;
+ }
+
+ if (!IsSingleLineEditor() &&
+ NS_FAILED(rv = EnsurePaddingBRElementInMultilineEditor())) {
+ NS_WARNING(
+ "TextEditor::EnsurePaddingBRElementInMultilineEditor() failed");
+ break;
+ }
+
+ rv = EnsureCaretNotAtEndOfTextNode();
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ break;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "TextEditor::EnsureCaretNotAtEndOfTextNode() failed, but ignored");
+ rv = NS_OK;
+ break;
+ }
+ DebugOnly<nsresult> rvIgnored =
+ EditorBase::OnEndHandlingTopLevelEditSubAction();
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::OnEndHandlingTopLevelEditSubAction() failed, but ignored");
+ MOZ_ASSERT(!GetTopLevelEditSubAction());
+ MOZ_ASSERT(GetDirectionOfTopLevelEditSubAction() == eNone);
+ return rv;
+}
+
+EditActionResult TextEditor::InsertLineFeedCharacterAtSelection() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor());
+ MOZ_ASSERT(!IsSingleLineEditor());
+
+ UndefineCaretBidiLevel();
+
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ if (mMaxTextLength >= 0) {
+ nsAutoString insertionString(u"\n"_ns);
+ EditActionResult result =
+ MaybeTruncateInsertionStringForMaxLength(insertionString);
+ if (result.Failed()) {
+ NS_WARNING(
+ "TextEditor::MaybeTruncateInsertionStringForMaxLength() failed");
+ return result;
+ }
+ if (result.Handled()) {
+ // Don't return as handled since we stopped inserting the line break.
+ return EditActionCanceled();
+ }
+ }
+
+ // if the selection isn't collapsed, delete it.
+ if (!SelectionRefPtr()->IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed");
+ return EditActionIgnored(rv);
+ }
+ }
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed");
+ return EditActionIgnored(rv);
+ }
+
+ // get the (collapsed) selection location
+ const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return EditActionIgnored(NS_ERROR_FAILURE);
+ }
+
+ EditorRawDOMPoint pointToInsert(firstRange->StartRef());
+ if (NS_WARN_IF(!pointToInsert.IsSet())) {
+ return EditActionIgnored(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(pointToInsert.IsSetAndValid());
+ MOZ_ASSERT(!pointToInsert.IsContainerHTMLElement(nsGkAtoms::br));
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return EditActionIgnored(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ // Don't change my selection in sub-transactions.
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ // Insert a linefeed character.
+ EditorRawDOMPoint pointAfterInsertedLineFeed;
+ rv = InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert,
+ &pointAfterInsertedLineFeed);
+ if (!pointAfterInsertedLineFeed.IsSet()) {
+ NS_WARNING(
+ "EditorBase::InsertTextWithTransaction(\\n) didn't return position of "
+ "inserted linefeed");
+ return EditActionIgnored(NS_ERROR_FAILURE);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::InsertTextWithTransaction(\\n) failed");
+ return EditActionIgnored(rv);
+ }
+
+ // set the selection to the correct location
+ MOZ_ASSERT(
+ !pointAfterInsertedLineFeed.GetChild(),
+ "After inserting text into a text node, pointAfterInsertedLineFeed."
+ "GetChild() should be nullptr");
+ rv = MOZ_KnownLive(SelectionRefPtr())
+ ->CollapseInLimiter(pointAfterInsertedLineFeed);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Selection::CollapseInLimiter() failed");
+ return EditActionIgnored(rv);
+ }
+
+ // XXX I don't think we still need this. This must have been required when
+ // `<textarea>` was implemented with text nodes and `<br>` elements.
+ // see if we're at the end of the editor range
+ EditorRawDOMPoint endPoint(EditorBase::GetEndPoint(*SelectionRefPtr()));
+ if (endPoint == pointAfterInsertedLineFeed) {
+ // SetInterlinePosition(true) means we want the caret to stick to the
+ // content on the "right". We want the caret to stick to whatever is
+ // past the break. This is because the break is on the same line we
+ // were on, but the next content will be on the following line.
+ IgnoredErrorResult ignoredError;
+ SelectionRefPtr()->SetInterlinePosition(true, ignoredError);
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "Selection::SetInterlinePosition(true) failed, but ignored");
+ }
+
+ return EditActionHandled();
+}
+
+nsresult TextEditor::EnsureCaretNotAtEndOfTextNode() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsPlaintextEditor());
+
+ // If there is no selection ranges, we should set to the end of the editor.
+ // This is usually performed in InitEditorContentAndSelection(), however,
+ // if the editor is reframed, this may be called by
+ // OnEndHandlingTopLevelEditSubAction().
+ if (!SelectionRefPtr()->RangeCount()) {
+ DebugOnly<nsresult> rvIgnored = CollapseSelectionToEnd();
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rvIgnored),
+ "EditorBase::CollapseSelectionToEnd() failed, but ignored");
+ }
+
+ // If we are at the end of the <textarea> element, we need to set the
+ // selection to stick to the padding <br> element for empty last line at the
+ // end of the <textarea>.
+ EditorRawDOMPoint selectionStartPoint(
+ EditorBase::GetStartPoint(*SelectionRefPtr()));
+ if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Nothing to do if we're not at the end of the text node.
+ if (!selectionStartPoint.IsInTextNode() ||
+ !selectionStartPoint.IsEndOfContainer()) {
+ return NS_OK;
+ }
+
+ Element* anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+ nsINode* parentNode = selectionStartPoint.GetContainer()->GetParentNode();
+ if (parentNode != anonymousDivElement) {
+ return NS_OK;
+ }
+
+ nsIContent* nextContent =
+ selectionStartPoint.GetContainer()->GetNextSibling();
+ if (!nextContent ||
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(*nextContent)) {
+ return NS_OK;
+ }
+
+ EditorRawDOMPoint afterStartContainer(
+ EditorRawDOMPoint::After(*selectionStartPoint.GetContainer()));
+ if (NS_WARN_IF(!afterStartContainer.IsSet())) {
+ return NS_ERROR_FAILURE;
+ }
+ IgnoredErrorResult ignoredError;
+ MOZ_KnownLive(SelectionRefPtr())
+ ->CollapseInLimiter(afterStartContainer, ignoredError);
+ if (NS_WARN_IF(Destroyed())) {
+ return NS_ERROR_EDITOR_DESTROYED;
+ }
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::CollapseInLimiter() failed");
+ return ignoredError.StealNSResult();
+}
+
+void TextEditor::HandleNewLinesInStringForSingleLineEditor(
+ nsString& aString) const {
+ static const char16_t kLF = static_cast<char16_t>('\n');
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsPlaintextEditor());
+ MOZ_ASSERT(aString.FindChar(static_cast<uint16_t>('\r')) == kNotFound);
+
+ // First of all, check if aString contains '\n' since if the string
+ // does not include it, we don't need to do nothing here.
+ int32_t firstLF = aString.FindChar(kLF, 0);
+ if (firstLF == kNotFound) {
+ return;
+ }
+
+ switch (mNewlineHandling) {
+ case nsIEditor::eNewlinesReplaceWithSpaces:
+ // Default of Firefox:
+ // Strip trailing newlines first so we don't wind up with trailing spaces
+ aString.Trim(LFSTR, false, true);
+ aString.ReplaceChar(kLF, ' ');
+ break;
+ case nsIEditor::eNewlinesStrip:
+ aString.StripChar(kLF);
+ break;
+ case nsIEditor::eNewlinesPasteToFirst:
+ default: {
+ // we get first *non-empty* line.
+ int32_t offset = 0;
+ while (firstLF == offset) {
+ offset++;
+ firstLF = aString.FindChar(kLF, offset);
+ }
+ if (firstLF > 0) {
+ aString.Truncate(firstLF);
+ }
+ if (offset > 0) {
+ aString.Cut(0, offset);
+ }
+ break;
+ }
+ case nsIEditor::eNewlinesReplaceWithCommas:
+ // Default of Thunderbird:
+ aString.Trim(LFSTR, true, true);
+ aString.ReplaceChar(kLF, ',');
+ break;
+ case nsIEditor::eNewlinesStripSurroundingWhitespace: {
+ nsAutoString result;
+ uint32_t offset = 0;
+ while (offset < aString.Length()) {
+ int32_t nextLF = !offset ? firstLF : aString.FindChar(kLF, offset);
+ if (nextLF < 0) {
+ result.Append(nsDependentSubstring(aString, offset));
+ break;
+ }
+ uint32_t wsBegin = nextLF;
+ // look backwards for the first non-white-space char
+ while (wsBegin > offset && NS_IS_SPACE(aString[wsBegin - 1])) {
+ --wsBegin;
+ }
+ result.Append(nsDependentSubstring(aString, offset, wsBegin - offset));
+ offset = nextLF + 1;
+ while (offset < aString.Length() && NS_IS_SPACE(aString[offset])) {
+ ++offset;
+ }
+ }
+ aString = result;
+ break;
+ }
+ case nsIEditor::eNewlinesPasteIntact:
+ // even if we're pasting newlines, don't paste leading/trailing ones
+ aString.Trim(LFSTR, true, true);
+ break;
+ }
+}
+
+EditActionResult TextEditor::HandleInsertText(
+ EditSubAction aEditSubAction, const nsAString& aInsertionString) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor());
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText ||
+ aEditSubAction == EditSubAction::eInsertTextComingFromIME);
+
+ UndefineCaretBidiLevel();
+
+ if (aInsertionString.IsEmpty() &&
+ aEditSubAction != EditSubAction::eInsertTextComingFromIME) {
+ // HACK: this is a fix for bug 19395
+ // I can't outlaw all empty insertions
+ // because IME transaction depend on them
+ // There is more work to do to make the
+ // world safe for IME.
+ return EditActionCanceled();
+ }
+
+ nsAutoString insertionString(aInsertionString);
+ if (mMaxTextLength >= 0) {
+ EditActionResult result =
+ MaybeTruncateInsertionStringForMaxLength(insertionString);
+ if (result.Failed()) {
+ NS_WARNING(
+ "TextEditor::MaybeTruncateInsertionStringForMaxLength() failed");
+ return result.MarkAsHandled();
+ }
+ // If we're exceeding the maxlength when composing IME, we need to clean up
+ // the composing text, so we shouldn't return early.
+ if (result.Handled() && insertionString.IsEmpty() &&
+ aEditSubAction != EditSubAction::eInsertTextComingFromIME) {
+ return EditActionCanceled();
+ }
+ }
+
+ uint32_t start = 0;
+ if (IsPasswordEditor()) {
+ if (GetComposition() && !GetComposition()->String().IsEmpty()) {
+ start = GetComposition()->XPOffsetInTextNode();
+ } else {
+ uint32_t end = 0;
+ nsContentUtils::GetSelectionInTextControl(SelectionRefPtr(), GetRoot(),
+ start, end);
+ }
+ }
+
+ // if the selection isn't collapsed, delete it.
+ if (!SelectionRefPtr()->IsCollapsed()) {
+ nsresult rv =
+ DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed");
+ return EditActionHandled(rv);
+ }
+ }
+
+ // XXX Why don't we cancel here? Shouldn't we do this first?
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ MaybeDoAutoPasswordMasking();
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed");
+ return EditActionHandled(rv);
+ }
+
+ // People have lots of different ideas about what text fields
+ // should do with multiline pastes. See bugs 21032, 23485, 23485, 50935.
+ // The six possible options are:
+ // 0. paste newlines intact
+ // 1. paste up to the first newline (default)
+ // 2. replace newlines with spaces
+ // 3. strip newlines
+ // 4. replace with commas
+ // 5. strip newlines and surrounding white-space
+ // So find out what we're expected to do:
+ if (IsSingleLineEditor()) {
+ // XXX Some callers of TextEditor::InsertTextAsAction() already make the
+ // string use only \n as a linebreaker. However, they are not hot
+ // path and nsContentUtils::PlatformToDOMLineBreaks() does nothing
+ // if the string doesn't include \r. So, let's convert linebreakers
+ // here. Note that there are too many callers of
+ // TextEditor::InsertTextAsAction(). So, it's difficult to keep
+ // maintaining all of them won't reach here without \r nor \r\n.
+ // XXX Should we handle do this before truncating the string for
+ // `maxlength`?
+ nsContentUtils::PlatformToDOMLineBreaks(insertionString);
+ HandleNewLinesInStringForSingleLineEditor(insertionString);
+ }
+
+ // get the (collapsed) selection location
+ const nsRange* firstRange = SelectionRefPtr()->GetRangeAt(0);
+ if (NS_WARN_IF(!firstRange)) {
+ return EditActionHandled(NS_ERROR_FAILURE);
+ }
+ EditorRawDOMPoint atStartOfSelection(firstRange->StartRef());
+ if (NS_WARN_IF(!atStartOfSelection.IsSetAndValid())) {
+ return EditActionHandled(NS_ERROR_FAILURE);
+ }
+ MOZ_ASSERT(!atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::br));
+
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return EditActionHandled(NS_ERROR_NOT_INITIALIZED);
+ }
+
+ if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) {
+ EditorRawDOMPoint compositionStartPoint = GetCompositionStartPoint();
+ if (!compositionStartPoint.IsSet()) {
+ compositionStartPoint = FindBetterInsertionPoint(atStartOfSelection);
+ NS_WARNING_ASSERTION(
+ compositionStartPoint.IsSet(),
+ "EditorBase::FindBetterInsertionPoint() failed, but ignored");
+ }
+ nsresult rv = InsertTextWithTransaction(*document, insertionString,
+ compositionStartPoint);
+ if (NS_WARN_IF(Destroyed())) {
+ return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextWithTransaction() failed");
+ return EditActionHandled(rv);
+ }
+ } else {
+ MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText);
+
+ // don't change my selection in subtransactions
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+
+ EditorRawDOMPoint pointAfterStringInserted;
+ nsresult rv = InsertTextWithTransaction(*document, insertionString,
+ atStartOfSelection,
+ &pointAfterStringInserted);
+ if (NS_WARN_IF(Destroyed())) {
+ return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertTextWithTransaction() failed");
+ return EditActionHandled(rv);
+ }
+
+ if (pointAfterStringInserted.IsSet()) {
+ // Make the caret attach to the inserted text, unless this text ends with
+ // a LF, in which case make the caret attach to the next line.
+ bool endsWithLF =
+ !insertionString.IsEmpty() && insertionString.Last() == nsCRT::LF;
+ IgnoredErrorResult ignoredError;
+ SelectionRefPtr()->SetInterlinePosition(endsWithLF, ignoredError);
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "Selection::SetInterlinePosition() failed, but ignored");
+
+ MOZ_ASSERT(
+ !pointAfterStringInserted.GetChild(),
+ "After inserting text into a text node, pointAfterStringInserted."
+ "GetChild() should be nullptr");
+ ignoredError = IgnoredErrorResult();
+ MOZ_KnownLive(SelectionRefPtr())
+ ->CollapseInLimiter(pointAfterStringInserted, ignoredError);
+ if (NS_WARN_IF(Destroyed())) {
+ return EditActionHandled(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ !ignoredError.Failed(),
+ "Selection::CollapseInLimiter() failed, but ignored");
+ }
+ }
+
+ // Unmask inputted character(s) if necessary.
+ if (IsPasswordEditor() && IsMaskingPassword() && CanEchoPasswordNow()) {
+ nsresult rv = SetUnmaskRangeAndNotify(start, insertionString.Length(),
+ LookAndFeel::GetPasswordMaskDelay());
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "TextEditor::SetUnmaskRangeAndNotify() failed");
+ return EditActionHandled(rv);
+ }
+
+ return EditActionHandled();
+}
+
+EditActionResult TextEditor::SetTextWithoutTransaction(
+ const nsAString& aValue) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(!AsHTMLEditor());
+ MOZ_ASSERT(IsPlaintextEditor());
+ MOZ_ASSERT(!IsIMEComposing());
+ MOZ_ASSERT(!IsUndoRedoEnabled());
+ MOZ_ASSERT(GetEditAction() != EditAction::eReplaceText);
+ MOZ_ASSERT(mMaxTextLength < 0);
+ MOZ_ASSERT(aValue.FindChar(static_cast<char16_t>('\r')) == kNotFound);
+
+ UndefineCaretBidiLevel();
+
+ // XXX If we're setting value, shouldn't we keep setting the new value here?
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ MaybeDoAutoPasswordMasking();
+
+ nsresult rv = EnsureNoPaddingBRElementForEmptyEditor();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::EnsureNoPaddingBRElementForEmptyEditor() failed");
+ return EditActionResult(rv);
+ }
+
+ RefPtr<Element> anonymousDivElement = GetRoot();
+ nsIContent* firstChild = anonymousDivElement->GetFirstChild();
+
+ // We can use this fast path only when:
+ // - we need to insert a text node.
+ // - we need to replace content of existing text node.
+ // Additionally, for avoiding odd result, we should check whether we're in
+ // usual condition.
+ if (IsSingleLineEditor()) {
+ // If we're a single line text editor, i.e., <input>, there is only padding
+ // <br> element. Otherwise, there should be only one text node. But note
+ // that even if there is a padding <br> element for empty editor, it's
+ // already been removed by `EnsureNoPaddingBRElementForEmptyEditor()`. So,
+ // at here, there should be only one text node or no children.
+ if (firstChild && (!firstChild->IsText() || firstChild->GetNextSibling())) {
+ return EditActionIgnored();
+ }
+ } else {
+ // If we're a multiline text editor, i.e., <textarea>, there is a padding
+ // <br> element for empty last line followed by scrollbar/resizer elements.
+ // Otherwise, a text node is followed by them.
+ if (!firstChild) {
+ return EditActionIgnored();
+ }
+ if (firstChild->IsText()) {
+ if (!firstChild->GetNextSibling() ||
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *firstChild->GetNextSibling())) {
+ return EditActionIgnored();
+ }
+ } else if (!EditorUtils::IsPaddingBRElementForEmptyLastLine(*firstChild)) {
+ return EditActionIgnored();
+ }
+ }
+
+ // XXX Password fields accept line breaks as normal characters with this code.
+ // Is this intentional?
+ nsAutoString sanitizedValue(aValue);
+ if (IsSingleLineEditor() && !IsPasswordEditor()) {
+ HandleNewLinesInStringForSingleLineEditor(sanitizedValue);
+ }
+
+ if (!firstChild || !firstChild->IsText()) {
+ if (sanitizedValue.IsEmpty()) {
+ return EditActionHandled();
+ }
+ RefPtr<Document> document = GetDocument();
+ if (NS_WARN_IF(!document)) {
+ return EditActionIgnored();
+ }
+ RefPtr<nsTextNode> newTextNode = CreateTextNode(sanitizedValue);
+ if (!newTextNode) {
+ NS_WARNING("EditorBase::CreateTextNode() failed");
+ return EditActionIgnored();
+ }
+ nsresult rv = InsertNodeWithTransaction(
+ *newTextNode, EditorDOMPoint(anonymousDivElement, 0));
+ if (NS_WARN_IF(Destroyed())) {
+ return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::InsertNodeWithTransaction() failed");
+ return EditActionResult(rv);
+ }
+ return EditActionHandled();
+ }
+
+ // TODO: If new value is empty string, we should only remove it.
+ RefPtr<Text> textNode = firstChild->GetAsText();
+ if (MOZ_UNLIKELY(!textNode)) {
+ NS_WARNING("The first child was not a text node");
+ return EditActionIgnored();
+ }
+ rv = SetTextNodeWithoutTransaction(sanitizedValue, *textNode);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("EditorBase::SetTextNodeWithoutTransaction() failed");
+ return EditActionResult(rv);
+ }
+
+ // If we replaced non-empty value with empty string, we need to delete the
+ // text node.
+ if (sanitizedValue.IsEmpty() && !textNode->Length()) {
+ nsresult rv = DeleteNodeWithTransaction(*textNode);
+ if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) {
+ return EditActionResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteNodeWithTransaction() failed, but ignored");
+ // XXX I don't think this is necessary because the anonymous `<div>`
+ // element has now only padding `<br>` element even if there are
+ // something.
+ IgnoredErrorResult ignoredError;
+ SelectionRefPtr()->SetInterlinePosition(true, ignoredError);
+ NS_WARNING_ASSERTION(!ignoredError.Failed(),
+ "Selection::SetInterlinePoisition(true) failed");
+ }
+
+ return EditActionHandled();
+}
+
+EditActionResult TextEditor::HandleDeleteSelection(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor());
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eNoStrip);
+
+ UndefineCaretBidiLevel();
+
+ CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY
+
+ // if there is only padding <br> element for empty editor, cancel the
+ // operation.
+ if (mPaddingBRElementForEmptyEditor) {
+ return EditActionCanceled();
+ }
+ EditActionResult result =
+ HandleDeleteSelectionInternal(aDirectionAndAmount, nsIEditor::eNoStrip);
+ // HandleDeleteSelectionInternal() creates SelectionBatcher. Therefore,
+ // quitting from it might cause having destroyed the editor.
+ if (NS_WARN_IF(Destroyed())) {
+ return result.SetResult(NS_ERROR_EDITOR_DESTROYED);
+ }
+ NS_WARNING_ASSERTION(
+ result.Succeeded(),
+ "TextEditor::HandleDeleteSelectionInternal(eNoStrip) failed");
+ return result;
+}
+
+EditActionResult TextEditor::HandleDeleteSelectionInternal(
+ nsIEditor::EDirection aDirectionAndAmount,
+ nsIEditor::EStripWrappers aStripWrappers) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsTextEditor());
+ MOZ_ASSERT(aStripWrappers == nsIEditor::eNoStrip);
+
+ // If the current selection is empty (e.g the user presses backspace with
+ // a collapsed selection), then we want to avoid sending the selectstart
+ // event to the user, so we hide selection changes. However, we still
+ // want to send a single selectionchange event to the document, so we
+ // batch the selectionchange events, such that a single event fires after
+ // the AutoHideSelectionChanges destructor has been run.
+ SelectionBatcher selectionBatcher(SelectionRefPtr());
+ AutoHideSelectionChanges hideSelection(SelectionRefPtr());
+ nsAutoScriptBlocker scriptBlocker;
+
+ if (IsPasswordEditor() && IsMaskingPassword()) {
+ MaskAllCharacters();
+ } else {
+ EditorRawDOMPoint selectionStartPoint(
+ EditorBase::GetStartPoint(*SelectionRefPtr()));
+ if (NS_WARN_IF(!selectionStartPoint.IsSet())) {
+ return EditActionResult(NS_ERROR_FAILURE);
+ }
+
+ if (!SelectionRefPtr()->IsCollapsed()) {
+ nsresult rv = DeleteSelectionWithTransaction(aDirectionAndAmount,
+ nsIEditor::eNoStrip);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteSelectionWithTransaction(eNoStrip) failed");
+ return EditActionHandled(rv);
+ }
+
+ // Test for distance between caret and text that will be deleted
+ AutoCaretBidiLevelManager bidiLevelManager(*this, aDirectionAndAmount,
+ selectionStartPoint);
+ if (bidiLevelManager.Failed()) {
+ NS_WARNING("EditorBase::AutoCaretBidiLevelManager() failed");
+ return EditActionResult(NS_ERROR_FAILURE);
+ }
+ bidiLevelManager.MaybeUpdateCaretBidiLevel(*this);
+ if (bidiLevelManager.Canceled()) {
+ return EditActionCanceled();
+ }
+ }
+
+ AutoRangeArray rangesToDelete(*SelectionRefPtr());
+ Result<nsIEditor::EDirection, nsresult> result =
+ rangesToDelete.ExtendAnchorFocusRangeFor(*this, aDirectionAndAmount);
+ if (result.isErr()) {
+ NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed");
+ return EditActionResult(result.unwrapErr());
+ }
+
+ nsresult rv = DeleteRangesWithTransaction(
+ result.unwrap(), nsIEditor::eNoStrip, rangesToDelete);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "EditorBase::DeleteRangesWithTransaction(eNoStrip) failed");
+ return EditActionHandled(rv);
+}
+
+EditActionResult TextEditor::ComputeValueFromTextNodeAndPaddingBRElement(
+ nsAString& aValue) const {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+
+ // If there is a padding <br> element, there's no content. So output empty
+ // string.
+ if (mPaddingBRElementForEmptyEditor) {
+ aValue.Truncate();
+ return EditActionHandled();
+ }
+
+ // If it's neither <input type="text"> nor <textarea>, e.g., an HTML editor
+ // which is in plaintext mode (e.g., plaintext email composer on Thunderbird),
+ // it should be handled by the expensive path.
+ if (AsHTMLEditor()) {
+ return EditActionIgnored();
+ }
+
+ Element* anonymousDivElement = GetRoot();
+ if (!anonymousDivElement) {
+ // Don't warn this case, this is possible, e.g., 997805.html
+ aValue.Truncate();
+ return EditActionHandled();
+ }
+
+ nsIContent* textNodeOrPaddingBRElement = anonymousDivElement->GetFirstChild();
+ if (!textNodeOrPaddingBRElement) {
+ aValue.Truncate();
+ return EditActionHandled();
+ }
+
+ // If it's an <input type="text"> element, the DOM tree should be:
+ // <div (::-moz-text-control-editing-root)>
+ // #text
+ // </div>
+ //
+ // If it's a <textarea> element, the DOM tree should be:
+ // <div (::-moz-text-control-editing-root)>
+ // #text (if there is)
+ // <br type="_moz">
+ // <scrollbar orient="horizontal">
+ // ...
+ // </div>
+
+ Text* textNode = textNodeOrPaddingBRElement->GetAsText();
+ if (!textNode) {
+ // If there is no text node in the expected DOM tree, we can say that it's
+ // just empty.
+ aValue.Truncate();
+ return EditActionHandled();
+ }
+
+ nsIContent* firstChildExceptText =
+ textNode ? textNodeOrPaddingBRElement->GetNextSibling()
+ : textNodeOrPaddingBRElement;
+ // If the DOM tree is unexpected, fall back to the expensive path.
+ bool isInput = IsSingleLineEditor();
+ bool isTextarea = !isInput;
+ if (NS_WARN_IF(isInput && firstChildExceptText) ||
+ NS_WARN_IF(isTextarea && !firstChildExceptText) ||
+ NS_WARN_IF(isTextarea &&
+ !EditorUtils::IsPaddingBRElementForEmptyLastLine(
+ *firstChildExceptText) &&
+ !firstChildExceptText->IsXULElement(nsGkAtoms::scrollbar))) {
+ return EditActionIgnored();
+ }
+
+ // Otherwise, the text data is the value.
+ textNode->GetData(aValue);
+ return EditActionHandled();
+}
+
+nsresult TextEditor::EnsurePaddingBRElementInMultilineEditor() {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(IsPlaintextEditor());
+ MOZ_ASSERT(!IsSingleLineEditor());
+
+ Element* anonymousDivElement = GetRoot();
+ if (NS_WARN_IF(!anonymousDivElement)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Assuming EditorBase::MaybeCreatePaddingBRElementForEmptyEditor() has been
+ // called first.
+ // XXX This assumption is wrong. This method may be called alone. Actually,
+ // we see this warning in mochitest log. So, we should fix this bug
+ // later.
+ if (NS_WARN_IF(!anonymousDivElement->GetLastChild())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<HTMLBRElement> brElement =
+ HTMLBRElement::FromNode(anonymousDivElement->GetLastChild());
+ if (!brElement) {
+ AutoTransactionsConserveSelection dontChangeMySelection(*this);
+ EditorDOMPoint endOfAnonymousDiv(
+ EditorDOMPoint::AtEndOf(*anonymousDivElement));
+ CreateElementResult createPaddingBRResult =
+ InsertPaddingBRElementForEmptyLastLineWithTransaction(
+ endOfAnonymousDiv);
+ NS_WARNING_ASSERTION(
+ createPaddingBRResult.Succeeded(),
+ "EditorBase::InsertPaddingBRElementForEmptyLastLineWithTransaction() "
+ "failed");
+ return createPaddingBRResult.Rv();
+ }
+
+ // Check to see if the trailing BR is a former padding <br> element for empty
+ // editor - this will have stuck around if we previously morphed a trailing
+ // node into a padding <br> element.
+ if (!brElement->IsPaddingForEmptyEditor()) {
+ return NS_OK;
+ }
+
+ // Morph it back to a padding <br> element for empty last line.
+ brElement->UnsetFlags(NS_PADDING_FOR_EMPTY_EDITOR);
+ brElement->SetFlags(NS_PADDING_FOR_EMPTY_LAST_LINE);
+
+ return NS_OK;
+}
+
+EditActionResult TextEditor::MaybeTruncateInsertionStringForMaxLength(
+ nsAString& aInsertionString) {
+ MOZ_ASSERT(IsEditActionDataAvailable());
+ MOZ_ASSERT(mMaxTextLength >= 0);
+
+ if (!IsPlaintextEditor() || IsIMEComposing()) {
+ return EditActionIgnored();
+ }
+
+ // Ignore user pastes
+ switch (GetEditAction()) {
+ case EditAction::ePaste:
+ case EditAction::ePasteAsQuotation:
+ case EditAction::eDrop:
+ case EditAction::eReplaceText:
+ // EditActionPrinciple() is non-null iff the edit was requested by
+ // javascript.
+ if (!GetEditActionPrincipal()) {
+ // By now we are certain that this is a user paste, before we ignore it,
+ // lets check if the user explictly enabled truncating user pastes.
+ if (!StaticPrefs::editor_truncate_user_pastes()) {
+ return EditActionIgnored();
+ }
+ }
+ [[fallthrough]];
+ default:
+ break;
+ }
+
+ int32_t currentLength = INT32_MAX;
+ nsresult rv = GetTextLength(&currentLength);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("TextEditor::GetTextLength() failed");
+ return EditActionResult(rv);
+ }
+
+ uint32_t selectionStart, selectionEnd;
+ nsContentUtils::GetSelectionInTextControl(SelectionRefPtr(), GetRoot(),
+ selectionStart, selectionEnd);
+
+ TextComposition* composition = GetComposition();
+ const uint32_t kOldCompositionStringLength =
+ composition ? composition->String().Length() : 0;
+
+ const uint32_t kSelectionLength = selectionEnd - selectionStart;
+ // XXX This computation must be wrong. If we'll support non-collapsed
+ // selection even during composition for Korean IME, kSelectionLength
+ // is part of kOldCompositionStringLength.
+ const uint32_t kNewLength =
+ currentLength - kSelectionLength - kOldCompositionStringLength;
+ if (kNewLength >= static_cast<uint32_t>(mMaxTextLength)) {
+ aInsertionString.Truncate(); // Too long, we cannot accept new character.
+ return EditActionHandled();
+ }
+
+ if (aInsertionString.Length() + kNewLength <=
+ static_cast<uint32_t>(mMaxTextLength)) {
+ return EditActionIgnored(); // Enough short string.
+ }
+
+ int32_t newInsertionStringLength = mMaxTextLength - kNewLength;
+ MOZ_ASSERT(newInsertionStringLength > 0);
+ char16_t maybeHighSurrogate =
+ aInsertionString.CharAt(newInsertionStringLength - 1);
+ char16_t maybeLowSurrogate =
+ aInsertionString.CharAt(newInsertionStringLength);
+ // Don't split the surrogate pair.
+ if (NS_IS_SURROGATE_PAIR(maybeHighSurrogate, maybeLowSurrogate)) {
+ newInsertionStringLength--;
+ }
+ // XXX What should we do if we're removing IVS but its preceding
+ // character won't be removed?
+ aInsertionString.Truncate(newInsertionStringLength);
+ return EditActionHandled();
+}
+
+bool TextEditor::CanEchoPasswordNow() const {
+ if (!LookAndFeel::GetEchoPassword() ||
+ (mFlags & nsIEditor::eEditorDontEchoPassword)) {
+ return false;
+ }
+
+ return GetEditAction() != EditAction::eDrop &&
+ GetEditAction() != EditAction::ePaste;
+}
+
+} // namespace mozilla