diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /editor/libeditor/TextEditSubActionHandler.cpp | |
parent | Initial commit. (diff) | |
download | firefox-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.cpp | 1043 |
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(¤tLength); + 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 |