diff options
Diffstat (limited to '')
-rw-r--r-- | editor/libeditor/TextEditSubActionHandler.cpp | 807 |
1 files changed, 807 insertions, 0 deletions
diff --git a/editor/libeditor/TextEditSubActionHandler.cpp b/editor/libeditor/TextEditSubActionHandler.cpp new file mode 100644 index 0000000000..5e064a123f --- /dev/null +++ b/editor/libeditor/TextEditSubActionHandler.cpp @@ -0,0 +1,807 @@ +/* -*- 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 "AutoRangeArray.h" +#include "EditAction.h" +#include "EditorDOMPoint.h" +#include "EditorUtils.h" +#include "HTMLEditor.h" + +#include "mozilla/Assertions.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 "nsISupports.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 EditActionResult::CanceledResult(); \ + } + +void TextEditor::OnStartToHandleTopLevelEditSubAction( + EditSubAction aTopLevelEditSubAction, + nsIEditor::EDirection aDirectionOfTopLevelEditSubAction, ErrorResult& aRv) { + MOZ_ASSERT(IsEditActionDataAvailable()); + 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. + const EditorRawDOMPoint point = + FindBetterInsertionPoint(EditorRawDOMPoint(SelectionRef().AnchorRef())); + NS_WARNING_ASSERTION( + point.IsSet(), + "EditorBase::FindBetterInsertionPoint() failed, but ignored"); + if (point.IsSet()) { + SetSpellCheckRestartPoint(point); + return; + } + } + if (SelectionRef().AnchorRef().IsSet()) { + SetSpellCheckRestartPoint(EditorRawDOMPoint(SelectionRef().AnchorRef())); + } +} + +nsresult TextEditor::OnEndHandlingTopLevelEditSubAction() { + MOZ_ASSERT(IsTopLevelEditSubActionDataAvailable()); + + 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 (!IsSingleLineEditor() && + NS_FAILED(rv = EnsurePaddingBRElementInMultilineEditor())) { + NS_WARNING( + "EditorBase::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; +} + +nsresult TextEditor::InsertLineBreakAsSubAction() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + if (NS_WARN_IF(!mInitSucceeded)) { + return NS_ERROR_NOT_INITIALIZED; + } + + IgnoredErrorResult ignoredError; + AutoEditSubActionNotifier startToHandleEditSubAction( + *this, EditSubAction::eInsertLineBreak, 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"); + + Result<EditActionResult, nsresult> result = + InsertLineFeedCharacterAtSelection(); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "TextEditor::InsertLineFeedCharacterAtSelection() failed, but ignored"); + return result.unwrapErr(); + } + return NS_OK; +} + +Result<EditActionResult, nsresult> +TextEditor::InsertLineFeedCharacterAtSelection() { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!IsSingleLineEditor()); + + UndefineCaretBidiLevel(); + + CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY + + if (mMaxTextLength >= 0) { + nsAutoString insertionString(u"\n"_ns); + Result<EditActionResult, nsresult> result = + MaybeTruncateInsertionStringForMaxLength(insertionString); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "TextEditor::MaybeTruncateInsertionStringForMaxLength() failed"); + return result; + } + if (result.inspect().Handled()) { + // Don't return as handled since we stopped inserting the line break. + return EditActionResult::CanceledResult(); + } + } + + // if the selection isn't collapsed, delete it. + if (!SelectionRef().IsCollapsed()) { + nsresult rv = + DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); + if (NS_FAILED(rv)) { + NS_WARNING( + "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed"); + return Err(rv); + } + } + + const auto pointToInsert = GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!pointToInsert.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + MOZ_ASSERT(pointToInsert.IsSetAndValid()); + MOZ_ASSERT(!pointToInsert.IsContainerHTMLElement(nsGkAtoms::br)); + + RefPtr<Document> document = GetDocument(); + if (NS_WARN_IF(!document)) { + return Err(NS_ERROR_NOT_INITIALIZED); + } + + // Don't change my selection in sub-transactions. + AutoTransactionsConserveSelection dontChangeMySelection(*this); + + // Insert a linefeed character. + Result<EditorDOMPoint, nsresult> insertTextResult = + InsertTextWithTransaction(*document, u"\n"_ns, pointToInsert); + if (MOZ_UNLIKELY(insertTextResult.isErr())) { + NS_WARNING("TextEditor::InsertTextWithTransaction(\"\\n\") failed"); + return insertTextResult.propagateErr(); + } + if (MOZ_UNLIKELY(!insertTextResult.inspect().IsSet())) { + NS_WARNING( + "EditorBase::InsertTextWithTransaction(\"\\n\") didn't return position " + "of inserted linefeed"); + return Err(NS_ERROR_FAILURE); + } + + // set the selection to the correct location + MOZ_ASSERT(insertTextResult.inspect().IsInTextNode(), + "After inserting text into a text node, insertTextResult should " + "return a point in a text node"); + nsresult rv = CollapseSelectionTo(insertTextResult.inspect()); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::CollapseSelectionTo() failed"); + return Err(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 + const auto endPoint = GetFirstSelectionEndPoint<EditorRawDOMPoint>(); + if (endPoint == insertTextResult.inspect()) { + // 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. + DebugOnly<nsresult> rvIgnored = + SelectionRef().SetInterlinePosition(InterlinePosition::StartOfNextLine); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Selection::SetInterlinePosition(InterlinePosition::" + "StartOfNextLine) failed, but ignored"); + } + + return EditActionResult::HandledResult(); +} + +nsresult TextEditor::EnsureCaretNotAtEndOfTextNode() { + MOZ_ASSERT(IsEditActionDataAvailable()); + + // 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 (SelectionRef().RangeCount()) { + return NS_OK; + } + + nsresult rv = CollapseSelectionToEndOfTextNode(); + if (MOZ_UNLIKELY(rv == NS_ERROR_EDITOR_DESTROYED)) { + NS_WARNING( + "TextEditor::CollapseSelectionToEndOfTextNode() caused destroying the " + "editor"); + return NS_ERROR_EDITOR_DESTROYED; + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "TextEditor::CollapseSelectionToEndOfTextNode() failed, but ignored"); + + return NS_OK; +} + +void TextEditor::HandleNewLinesInStringForSingleLineEditor( + nsString& aString) const { + static const char16_t kLF = static_cast<char16_t>('\n'); + MOZ_ASSERT(IsEditActionDataAvailable()); + 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; + } +} + +Result<EditActionResult, nsresult> TextEditor::HandleInsertText( + EditSubAction aEditSubAction, const nsAString& aInsertionString, + SelectionHandling aSelectionHandling) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText || + aEditSubAction == EditSubAction::eInsertTextComingFromIME); + MOZ_ASSERT_IF(aSelectionHandling == SelectionHandling::Ignore, + 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 EditActionResult::CanceledResult(); + } + + nsAutoString insertionString(aInsertionString); + if (mMaxTextLength >= 0) { + Result<EditActionResult, nsresult> result = + MaybeTruncateInsertionStringForMaxLength(insertionString); + if (MOZ_UNLIKELY(result.isErr())) { + NS_WARNING( + "TextEditor::MaybeTruncateInsertionStringForMaxLength() failed"); + EditActionResult unwrappedResult = result.unwrap(); + unwrappedResult.MarkAsHandled(); + return unwrappedResult; + } + // 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.inspect().Handled() && insertionString.IsEmpty() && + aEditSubAction != EditSubAction::eInsertTextComingFromIME) { + return EditActionResult::CanceledResult(); + } + } + + uint32_t start = 0; + if (IsPasswordEditor()) { + if (GetComposition() && !GetComposition()->String().IsEmpty()) { + start = GetComposition()->XPOffsetInTextNode(); + } else { + uint32_t end = 0; + nsContentUtils::GetSelectionInTextControl(&SelectionRef(), GetRoot(), + start, end); + } + } + + // if the selection isn't collapsed, delete it. + if (!SelectionRef().IsCollapsed() && + aSelectionHandling == SelectionHandling::Delete) { + nsresult rv = + DeleteSelectionAsSubAction(nsIEditor::eNone, nsIEditor::eNoStrip); + if (NS_FAILED(rv)) { + NS_WARNING( + "EditorBase::DeleteSelectionAsSubAction(eNone, eNoStrip) failed"); + return Err(rv); + } + } + + // XXX Why don't we cancel here? Shouldn't we do this first? + CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY + + MaybeDoAutoPasswordMasking(); + + // 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); + } + + const auto atStartOfSelection = GetFirstSelectionStartPoint<EditorDOMPoint>(); + if (NS_WARN_IF(!atStartOfSelection.IsSetAndValid())) { + return Err(NS_ERROR_FAILURE); + } + MOZ_ASSERT(!atStartOfSelection.IsContainerHTMLElement(nsGkAtoms::br)); + + RefPtr<Document> document = GetDocument(); + if (NS_WARN_IF(!document)) { + return Err(NS_ERROR_NOT_INITIALIZED); + } + + if (aEditSubAction == EditSubAction::eInsertTextComingFromIME) { + EditorDOMPoint compositionStartPoint = + GetFirstIMESelectionStartPoint<EditorDOMPoint>(); + if (!compositionStartPoint.IsSet()) { + compositionStartPoint = FindBetterInsertionPoint(atStartOfSelection); + NS_WARNING_ASSERTION( + compositionStartPoint.IsSet(), + "EditorBase::FindBetterInsertionPoint() failed, but ignored"); + } + Result<EditorDOMPoint, nsresult> insertTextResult = + InsertTextWithTransaction(*document, insertionString, + compositionStartPoint); + if (MOZ_UNLIKELY(insertTextResult.isErr())) { + NS_WARNING("EditorBase::InsertTextWithTransaction() failed"); + return insertTextResult.propagateErr(); + } + } else { + MOZ_ASSERT(aEditSubAction == EditSubAction::eInsertText); + + // don't change my selection in subtransactions + AutoTransactionsConserveSelection dontChangeMySelection(*this); + + Result<EditorDOMPoint, nsresult> insertTextResult = + InsertTextWithTransaction(*document, insertionString, + atStartOfSelection); + if (MOZ_UNLIKELY(insertTextResult.isErr())) { + NS_WARNING("EditorBase::InsertTextWithTransaction() failed"); + return insertTextResult.propagateErr(); + } + + if (insertTextResult.inspect().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. + const bool endsWithLF = + !insertionString.IsEmpty() && insertionString.Last() == nsCRT::LF; + EditorDOMPoint pointToPutCaret = insertTextResult.unwrap(); + pointToPutCaret.SetInterlinePosition( + endsWithLF ? InterlinePosition::StartOfNextLine + : InterlinePosition::EndOfLine); + MOZ_ASSERT(pointToPutCaret.IsInTextNode(), + "After inserting text into a text node, insertTextResult " + "should return a point in a text node"); + nsresult rv = CollapseSelectionTo(pointToPutCaret); + if (NS_WARN_IF(rv == NS_ERROR_EDITOR_DESTROYED)) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "EditorBase::CollapseSelectionTo() failed, but ignored"); + } + } + + // Unmask inputted character(s) if necessary. + if (IsPasswordEditor() && IsMaskingPassword() && CanEchoPasswordNow()) { + nsresult rv = SetUnmaskRangeAndNotify(start, insertionString.Length(), + LookAndFeel::GetPasswordMaskDelay()); + if (NS_FAILED(rv)) { + NS_WARNING("TextEditor::SetUnmaskRangeAndNotify() failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); + } + + return EditActionResult::HandledResult(); +} + +Result<EditActionResult, nsresult> TextEditor::SetTextWithoutTransaction( + const nsAString& aValue) { + MOZ_ASSERT(IsEditActionDataAvailable()); + 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(); + + RefPtr<Element> anonymousDivElement = GetRoot(); + RefPtr<Text> textNode = + Text::FromNodeOrNull(anonymousDivElement->GetFirstChild()); + MOZ_ASSERT(textNode); + + // 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 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 (!textNode->GetNextSibling() || + !EditorUtils::IsPaddingBRElementForEmptyLastLine( + *textNode->GetNextSibling())) { + return EditActionResult::IgnoredResult(); + } + } + + // XXX Password fields accept line breaks as normal characters with this code. + // Is this intentional? + nsAutoString sanitizedValue(aValue); + if (IsSingleLineEditor() && !IsPasswordEditor()) { + HandleNewLinesInStringForSingleLineEditor(sanitizedValue); + } + + nsresult rv = SetTextNodeWithoutTransaction(sanitizedValue, *textNode); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::SetTextNodeWithoutTransaction() failed"); + return Err(rv); + } + + return EditActionResult::HandledResult(); +} + +Result<EditActionResult, nsresult> TextEditor::HandleDeleteSelection( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(aStripWrappers == nsIEditor::eNoStrip); + + UndefineCaretBidiLevel(); + + CANCEL_OPERATION_AND_RETURN_EDIT_ACTION_RESULT_IF_READONLY + + if (IsEmpty()) { + return EditActionResult::CanceledResult(); + } + Result<EditActionResult, nsresult> result = + HandleDeleteSelectionInternal(aDirectionAndAmount, nsIEditor::eNoStrip); + // HandleDeleteSelectionInternal() creates SelectionBatcher. Therefore, + // quitting from it might cause having destroyed the editor. + if (NS_WARN_IF(Destroyed())) { + return Err(NS_ERROR_EDITOR_DESTROYED); + } + NS_WARNING_ASSERTION( + result.isOk(), + "TextEditor::HandleDeleteSelectionInternal(eNoStrip) failed"); + return result; +} + +Result<EditActionResult, nsresult> TextEditor::HandleDeleteSelectionInternal( + nsIEditor::EDirection aDirectionAndAmount, + nsIEditor::EStripWrappers aStripWrappers) { + MOZ_ASSERT(IsEditActionDataAvailable()); + 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(SelectionRef(), __FUNCTION__); + AutoHideSelectionChanges hideSelection(SelectionRef()); + nsAutoScriptBlocker scriptBlocker; + + if (IsPasswordEditor() && IsMaskingPassword()) { + MaskAllCharacters(); + } else { + const auto selectionStartPoint = + GetFirstSelectionStartPoint<EditorRawDOMPoint>(); + if (NS_WARN_IF(!selectionStartPoint.IsSet())) { + return Err(NS_ERROR_FAILURE); + } + + if (!SelectionRef().IsCollapsed()) { + nsresult rv = DeleteSelectionWithTransaction(aDirectionAndAmount, + nsIEditor::eNoStrip); + if (NS_FAILED(rv)) { + NS_WARNING( + "EditorBase::DeleteSelectionWithTransaction(eNoStrip) failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); + } + + // Test for distance between caret and text that will be deleted + AutoCaretBidiLevelManager bidiLevelManager(*this, aDirectionAndAmount, + selectionStartPoint); + if (MOZ_UNLIKELY(bidiLevelManager.Failed())) { + NS_WARNING("EditorBase::AutoCaretBidiLevelManager() failed"); + return Err(NS_ERROR_FAILURE); + } + bidiLevelManager.MaybeUpdateCaretBidiLevel(*this); + if (bidiLevelManager.Canceled()) { + return EditActionResult::CanceledResult(); + } + } + + AutoRangeArray rangesToDelete(SelectionRef()); + Result<nsIEditor::EDirection, nsresult> result = + rangesToDelete.ExtendAnchorFocusRangeFor(*this, aDirectionAndAmount); + if (result.isErr()) { + NS_WARNING("AutoRangeArray::ExtendAnchorFocusRangeFor() failed"); + return result.propagateErr(); + } + if (const Text* theTextNode = GetTextNode()) { + rangesToDelete.EnsureRangesInTextNode(*theTextNode); + } + + nsresult rv = DeleteRangesWithTransaction( + result.unwrap(), nsIEditor::eNoStrip, rangesToDelete); + if (NS_FAILED(rv)) { + NS_WARNING("EditorBase::DeleteRangesWithTransaction(eNoStrip) failed"); + return Err(rv); + } + return EditActionResult::HandledResult(); +} + +Result<EditActionResult, nsresult> +TextEditor::ComputeValueFromTextNodeAndBRElement(nsAString& aValue) const { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(!IsHTMLEditor()); + + Element* anonymousDivElement = GetRoot(); + if (MOZ_UNLIKELY(!anonymousDivElement)) { + // Don't warn this case, this is possible, e.g., 997805.html + aValue.Truncate(); + return EditActionResult::HandledResult(); + } + + Text* textNode = Text::FromNodeOrNull(anonymousDivElement->GetFirstChild()); + MOZ_ASSERT(textNode); + + if (!textNode->Length()) { + aValue.Truncate(); + return EditActionResult::HandledResult(); + } + + nsIContent* firstChildExceptText = textNode->GetNextSibling(); + // 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 EditActionResult::IgnoredResult(); + } + + // Otherwise, the text data is the value. + textNode->GetData(aValue); + return EditActionResult::HandledResult(); +} + +Result<EditActionResult, nsresult> +TextEditor::MaybeTruncateInsertionStringForMaxLength( + nsAString& aInsertionString) { + MOZ_ASSERT(IsEditActionDataAvailable()); + MOZ_ASSERT(mMaxTextLength >= 0); + + if (IsIMEComposing()) { + return EditActionResult::IgnoredResult(); + } + + // 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 EditActionResult::IgnoredResult(); + } + } + [[fallthrough]]; + default: + break; + } + + uint32_t currentLength = UINT32_MAX; + nsresult rv = GetTextLength(¤tLength); + if (NS_FAILED(rv)) { + NS_WARNING("TextEditor::GetTextLength() failed"); + return Err(rv); + } + + uint32_t selectionStart, selectionEnd; + nsContentUtils::GetSelectionInTextControl(&SelectionRef(), 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 >= AssertedCast<uint32_t>(mMaxTextLength)) { + aInsertionString.Truncate(); // Too long, we cannot accept new character. + return EditActionResult::HandledResult(); + } + + if (aInsertionString.Length() + kNewLength <= + AssertedCast<uint32_t>(mMaxTextLength)) { + return EditActionResult::IgnoredResult(); // 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 EditActionResult::HandledResult(); +} + +bool TextEditor::CanEchoPasswordNow() const { + if (!LookAndFeel::GetEchoPassword() || EchoingPasswordPrevented()) { + return false; + } + + return GetEditAction() != EditAction::eDrop && + GetEditAction() != EditAction::ePaste; +} + +} // namespace mozilla |