mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
Bug 1149826 - Part 1. Add eContentCommandReplaceText. r=masayuki
When using autocorrect, we should use `insertReplacementText` according to https://github.com/w3c/input-events/issues/152. So I would like to add eContentCommandReplaceText command for this. Also, this command has an option that is source string text. When processing text subsitution, parent process doesn't know whether target replaced text is modified. So I add this option for check. Differential Revision: https://phabricator.services.mozilla.com/D213511
This commit is contained in:
parent
bb6b5c3a59
commit
6b14dceb59
@ -2566,7 +2566,10 @@ nsDOMWindowUtils::SendSelectionSetEvent(uint32_t aOffset, uint32_t aLength,
|
||||
NS_IMETHODIMP
|
||||
nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType,
|
||||
nsITransferable* aTransferable,
|
||||
const nsAString& aString) {
|
||||
const nsAString& aString,
|
||||
uint32_t aOffset,
|
||||
const nsAString& aReplaceSrcString,
|
||||
uint32_t aAdditionalFlags) {
|
||||
// get the widget to send the event to
|
||||
nsCOMPtr<nsIWidget> widget = GetWidget();
|
||||
if (!widget) return NS_ERROR_FAILURE;
|
||||
@ -2586,6 +2589,8 @@ nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType,
|
||||
msg = eContentCommandRedo;
|
||||
} else if (aType.EqualsLiteral("insertText")) {
|
||||
msg = eContentCommandInsertText;
|
||||
} else if (aType.EqualsLiteral("replaceText")) {
|
||||
msg = eContentCommandReplaceText;
|
||||
} else if (aType.EqualsLiteral("pasteTransferable")) {
|
||||
msg = eContentCommandPasteTransferable;
|
||||
} else {
|
||||
@ -2595,8 +2600,13 @@ nsDOMWindowUtils::SendContentCommandEvent(const nsAString& aType,
|
||||
WidgetContentCommandEvent event(true, msg, widget);
|
||||
if (msg == eContentCommandInsertText) {
|
||||
event.mString.emplace(aString);
|
||||
}
|
||||
if (msg == eContentCommandPasteTransferable) {
|
||||
} else if (msg == eContentCommandReplaceText) {
|
||||
event.mString.emplace(aString);
|
||||
event.mSelection.mReplaceSrcString = aReplaceSrcString;
|
||||
event.mSelection.mOffset = aOffset;
|
||||
event.mSelection.mPreventSetSelection =
|
||||
!!(aAdditionalFlags & CONTENT_COMMAND_FLAG_PREVENT_SET_SELECTION);
|
||||
} else if (msg == eContentCommandPasteTransferable) {
|
||||
event.mTransferable = aTransferable;
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include "mozilla/IMEStateManager.h"
|
||||
#include "mozilla/IntegerRange.h"
|
||||
#include "mozilla/Maybe.h"
|
||||
#include "mozilla/MiscEvents.h"
|
||||
#include "mozilla/PresShell.h"
|
||||
#include "mozilla/RangeBoundary.h"
|
||||
#include "mozilla/RangeUtils.h"
|
||||
@ -23,6 +24,7 @@
|
||||
#include "mozilla/dom/HTMLBRElement.h"
|
||||
#include "mozilla/dom/HTMLUnknownElement.h"
|
||||
#include "mozilla/dom/Selection.h"
|
||||
#include "mozilla/dom/StaticRange.h"
|
||||
#include "mozilla/dom/Text.h"
|
||||
#include "nsCaret.h"
|
||||
#include "nsCOMPtr.h"
|
||||
@ -857,6 +859,20 @@ nsresult ContentEventHandler::GenerateFlatTextContent(
|
||||
return GenerateFlatTextContent(rawRange, aString, aLineBreakType);
|
||||
}
|
||||
|
||||
nsresult ContentEventHandler::GenerateFlatTextContent(const nsRange* aRange,
|
||||
nsString& aString) {
|
||||
MOZ_ASSERT(aString.IsEmpty());
|
||||
|
||||
if (NS_WARN_IF(!aRange)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
UnsafeSimpleRange rawRange;
|
||||
rawRange.SetStartAndEnd(aRange);
|
||||
|
||||
return GenerateFlatTextContent(rawRange, aString, LINE_BREAK_TYPE_NATIVE);
|
||||
}
|
||||
|
||||
template <typename NodeType, typename RangeBoundaryType>
|
||||
nsresult ContentEventHandler::GenerateFlatTextContent(
|
||||
const SimpleRangeBase<NodeType, RangeBoundaryType>& aSimpleRange,
|
||||
@ -1158,6 +1174,27 @@ nsresult ContentEventHandler::ExpandToClusterBoundary(
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
already_AddRefed<nsRange> ContentEventHandler::GetRangeFromFlatTextOffset(
|
||||
WidgetContentCommandEvent* aEvent, uint32_t aOffset, uint32_t aLength) {
|
||||
nsresult rv = InitCommon(aEvent->mMessage);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Result<DOMRangeAndAdjustedOffsetInFlattenedText, nsresult> result =
|
||||
ConvertFlatTextOffsetToDOMRange(aOffset, aLength, LINE_BREAK_TYPE_NATIVE,
|
||||
false);
|
||||
if (NS_WARN_IF(result.isErr())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DOMRangeAndAdjustedOffsetInFlattenedText domRangeAndAdjustOffset =
|
||||
result.unwrap();
|
||||
|
||||
return nsRange::Create(domRangeAndAdjustOffset.mRange.Start(),
|
||||
domRangeAndAdjustOffset.mRange.End(), IgnoreErrors());
|
||||
}
|
||||
|
||||
template <typename RangeType, typename TextNodeType>
|
||||
Result<ContentEventHandler::DOMRangeAndAdjustedOffsetInFlattenedTextBase<
|
||||
RangeType, TextNodeType>,
|
||||
|
@ -365,6 +365,14 @@ class MOZ_STACK_CLASS ContentEventHandler {
|
||||
|
||||
static uint32_t GetNativeTextLength(const nsAString& aText);
|
||||
|
||||
// Get the range between start offset and end offset
|
||||
MOZ_CAN_RUN_SCRIPT
|
||||
already_AddRefed<nsRange> GetRangeFromFlatTextOffset(
|
||||
WidgetContentCommandEvent* aEvent, uint32_t aOffset, uint32_t aLength);
|
||||
|
||||
// Get the contents of aRange as plain text.
|
||||
nsresult GenerateFlatTextContent(const nsRange* aRange, nsString& aString);
|
||||
|
||||
protected:
|
||||
// Get the text length of aTextNode.
|
||||
static uint32_t GetTextLength(const dom::Text& aTextNode,
|
||||
|
@ -1292,6 +1292,9 @@ nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
|
||||
case eContentCommandInsertText:
|
||||
DoContentCommandInsertTextEvent(aEvent->AsContentCommandEvent());
|
||||
break;
|
||||
case eContentCommandReplaceText:
|
||||
DoContentCommandReplaceTextEvent(aEvent->AsContentCommandEvent());
|
||||
break;
|
||||
case eContentCommandScroll:
|
||||
DoContentCommandScrollEvent(aEvent->AsContentCommandEvent());
|
||||
break;
|
||||
@ -6847,6 +6850,85 @@ nsresult EventStateManager::DoContentCommandInsertTextEvent(
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult EventStateManager::DoContentCommandReplaceTextEvent(
|
||||
WidgetContentCommandEvent* aEvent) {
|
||||
MOZ_ASSERT(aEvent);
|
||||
MOZ_ASSERT(aEvent->mMessage == eContentCommandReplaceText);
|
||||
MOZ_DIAGNOSTIC_ASSERT(aEvent->mString.isSome());
|
||||
MOZ_DIAGNOSTIC_ASSERT(!aEvent->mString.ref().IsEmpty());
|
||||
|
||||
aEvent->mIsEnabled = false;
|
||||
aEvent->mSucceeded = false;
|
||||
|
||||
NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE);
|
||||
|
||||
if (XRE_IsParentProcess()) {
|
||||
// Handle it in focused content process if there is.
|
||||
if (BrowserParent* remote = BrowserParent::GetFocused()) {
|
||||
Unused << remote->SendReplaceText(
|
||||
aEvent->mSelection.mReplaceSrcString, aEvent->mString.ref(),
|
||||
aEvent->mSelection.mOffset, aEvent->mSelection.mPreventSetSelection);
|
||||
aEvent->mIsEnabled = true; // XXX it can be a lie...
|
||||
aEvent->mSucceeded = true;
|
||||
return NS_OK;
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no active editor in this process, we should treat the command
|
||||
// is disabled.
|
||||
RefPtr<EditorBase> activeEditor =
|
||||
nsContentUtils::GetActiveEditor(mPresContext);
|
||||
if (NS_WARN_IF(!activeEditor)) {
|
||||
aEvent->mSucceeded = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
RefPtr<TextComposition> composition =
|
||||
IMEStateManager::GetTextCompositionFor(mPresContext);
|
||||
if (NS_WARN_IF(composition)) {
|
||||
// We don't support replace text action during composition.
|
||||
aEvent->mSucceeded = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
ContentEventHandler handler(mPresContext);
|
||||
RefPtr<nsRange> range = handler.GetRangeFromFlatTextOffset(
|
||||
aEvent, aEvent->mSelection.mOffset,
|
||||
aEvent->mSelection.mReplaceSrcString.Length());
|
||||
if (NS_WARN_IF(!range)) {
|
||||
aEvent->mSucceeded = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// If original replacement text isn't matched with selection text, throws
|
||||
// error.
|
||||
nsAutoString targetStr;
|
||||
nsresult rv = handler.GenerateFlatTextContent(range, targetStr);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
aEvent->mSucceeded = false;
|
||||
return NS_OK;
|
||||
}
|
||||
if (!aEvent->mSelection.mReplaceSrcString.Equals(targetStr)) {
|
||||
aEvent->mSucceeded = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
rv = activeEditor->ReplaceTextAsAction(
|
||||
aEvent->mString.ref(), range,
|
||||
TextEditor::AllowBeforeInputEventCancelable::Yes,
|
||||
aEvent->mSelection.mPreventSetSelection
|
||||
? EditorBase::PreventSetSelection::Yes
|
||||
: EditorBase::PreventSetSelection::No);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||||
aEvent->mSucceeded = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
aEvent->mIsEnabled = rv != NS_SUCCESS_DOM_NO_OPERATION;
|
||||
aEvent->mSucceeded = true;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult EventStateManager::DoContentCommandScrollEvent(
|
||||
WidgetContentCommandEvent* aEvent) {
|
||||
NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE);
|
||||
|
@ -1219,6 +1219,8 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
|
||||
nsresult DoContentCommandEvent(WidgetContentCommandEvent* aEvent);
|
||||
MOZ_CAN_RUN_SCRIPT
|
||||
nsresult DoContentCommandInsertTextEvent(WidgetContentCommandEvent* aEvent);
|
||||
MOZ_CAN_RUN_SCRIPT
|
||||
nsresult DoContentCommandReplaceTextEvent(WidgetContentCommandEvent* aEvent);
|
||||
nsresult DoContentCommandScrollEvent(WidgetContentCommandEvent* aEvent);
|
||||
|
||||
dom::BrowserParent* GetCrossProcessTarget();
|
||||
|
@ -2809,8 +2809,7 @@ bool TextControlState::SetValueWithTextEditor(
|
||||
aHandlingSetValue.GetSettingValue(), nullptr,
|
||||
StaticPrefs::dom_input_event_allow_to_cancel_set_user_input()
|
||||
? TextEditor::AllowBeforeInputEventCancelable::Yes
|
||||
: TextEditor::AllowBeforeInputEventCancelable::No,
|
||||
nullptr);
|
||||
: TextEditor::AllowBeforeInputEventCancelable::No);
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
||||
"EditorBase::ReplaceTextAsAction() failed");
|
||||
return rv != NS_ERROR_OUT_OF_MEMORY;
|
||||
|
@ -1247,6 +1247,14 @@ interface nsIDOMWindowUtils : nsISupports {
|
||||
*/
|
||||
[implicit_jscontext] string getClassName(in jsval aObject);
|
||||
|
||||
/**
|
||||
* If sendContentCommanedEvent()'s aAdditionalFlags argument has no
|
||||
* CONTENT_COMMAND_FLAG_PREVENT_SET_SELECTION, after executing replaceText,
|
||||
* selection is next of replaced text. If set this with aReplaceSrcString,
|
||||
* we keeps selection position if selection isn't into replaced text.
|
||||
*/
|
||||
const unsigned long CONTENT_COMMAND_FLAG_PREVENT_SET_SELECTION = 0x0002;
|
||||
|
||||
/**
|
||||
* Generate a content command event.
|
||||
*
|
||||
@ -1254,16 +1262,22 @@ interface nsIDOMWindowUtils : nsISupports {
|
||||
* Will throw a DOM security error if called without chrome privileges.
|
||||
*
|
||||
* @param aType Type of command content event to send. Can be one of "cut",
|
||||
* "copy", "paste", "delete", "undo", "redo", "insertText" or
|
||||
* "pasteTransferable".
|
||||
* "copy", "paste", "delete", "undo", "redo", "insertText",
|
||||
* "pasteTransferable", or "replaceText"
|
||||
* @param aTransferable an instance of nsITransferable when aType is
|
||||
* "pasteTransferable"
|
||||
* @param aString The string to be inserted into focused editor when aType is
|
||||
* "insertText"
|
||||
* "insertText" or "replaceText"
|
||||
* @Param aOffset The relative to start of selection
|
||||
* @param aReplaceSrcString the source string of replaceText. If not matched, do nothing.
|
||||
* @param aAdditionalFlags See the description of CONTENT_COMMAND_FLAG_*.
|
||||
*/
|
||||
void sendContentCommandEvent(in AString aType,
|
||||
[optional] in nsITransferable aTransferable,
|
||||
[optional] in AString aString);
|
||||
[optional] in AString aString,
|
||||
[optional] in uint32_t aOffset,
|
||||
[optional] in AString aReplaceSrcString,
|
||||
[optional] in unsigned long aAdditionalFlags);
|
||||
|
||||
/**
|
||||
* If sendQueryContentEvent()'s aAdditionalFlags argument is
|
||||
|
@ -2259,6 +2259,27 @@ mozilla::ipc::IPCResult BrowserChild::RecvNormalPriorityInsertText(
|
||||
return RecvInsertText(aStringToInsert);
|
||||
}
|
||||
|
||||
mozilla::ipc::IPCResult BrowserChild::RecvReplaceText(
|
||||
const nsString& aReplaceSrcString, const nsString& aStringToInsert,
|
||||
uint32_t aOffset, bool aPreventSetSelection) {
|
||||
// Use normal event path to reach focused document.
|
||||
WidgetContentCommandEvent localEvent(true, eContentCommandReplaceText,
|
||||
mPuppetWidget);
|
||||
localEvent.mString = Some(aStringToInsert);
|
||||
localEvent.mSelection.mReplaceSrcString = aReplaceSrcString;
|
||||
localEvent.mSelection.mOffset = aOffset;
|
||||
localEvent.mSelection.mPreventSetSelection = aPreventSetSelection;
|
||||
DispatchWidgetEventViaAPZ(localEvent);
|
||||
return IPC_OK();
|
||||
}
|
||||
|
||||
mozilla::ipc::IPCResult BrowserChild::RecvNormalPriorityReplaceText(
|
||||
const nsString& aReplaceSrcString, const nsString& aStringToInsert,
|
||||
uint32_t aOffset, bool aPreventSetSelection) {
|
||||
return RecvReplaceText(aReplaceSrcString, aStringToInsert, aOffset,
|
||||
aPreventSetSelection);
|
||||
}
|
||||
|
||||
mozilla::ipc::IPCResult BrowserChild::RecvPasteTransferable(
|
||||
const IPCTransferable& aTransferable) {
|
||||
nsresult rv;
|
||||
|
@ -405,6 +405,15 @@ class BrowserChild final : public nsMessageManagerScriptExecutor,
|
||||
mozilla::ipc::IPCResult RecvNormalPriorityInsertText(
|
||||
const nsAString& aStringToInsert);
|
||||
|
||||
mozilla::ipc::IPCResult RecvReplaceText(const nsString& aReplaceSrcString,
|
||||
const nsString& aStringToInsert,
|
||||
uint32_t aOffset,
|
||||
bool aPreventSetSelection);
|
||||
|
||||
mozilla::ipc::IPCResult RecvNormalPriorityReplaceText(
|
||||
const nsString& aReplaceSrcString, const nsString& aStringToInsert,
|
||||
uint32_t aOffset, bool aPreventSetSelection);
|
||||
|
||||
MOZ_CAN_RUN_SCRIPT_BOUNDARY
|
||||
mozilla::ipc::IPCResult RecvPasteTransferable(
|
||||
const IPCTransferable& aTransferable);
|
||||
|
@ -853,6 +853,18 @@ child:
|
||||
[Priority=input] async InsertText(nsString aStringToInsert);
|
||||
async NormalPriorityInsertText(nsString aStringToInsert);
|
||||
|
||||
/**
|
||||
* Dispatch eContentCommandReplaceText event in the remote process.
|
||||
*/
|
||||
[Priority=input] async ReplaceText(nsString aReplacementString,
|
||||
nsString aStringToInsert,
|
||||
uint32_t aOffset,
|
||||
bool aPreventSetSelection);
|
||||
async NormalPriorityReplaceText(nsString aReplacementString,
|
||||
nsString aStringToInsert,
|
||||
uint32_t aOffset,
|
||||
bool aPreventSetSelection);
|
||||
|
||||
/**
|
||||
* Call PasteTransferable via a controller on the content process
|
||||
* to handle the command content event, "pasteTransferable".
|
||||
|
40
editor/libeditor/AutoSelectionRestorer.cpp
Normal file
40
editor/libeditor/AutoSelectionRestorer.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
/* -*- 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 "AutoSelectionRestorer.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
AutoSelectionRestorer::AutoSelectionRestorer(EditorBase* aEditor) {
|
||||
if (!aEditor) {
|
||||
return;
|
||||
}
|
||||
if (aEditor->ArePreservingSelection()) {
|
||||
// We already have initialized mParentData::mSavedSelection, so this must
|
||||
// be nested call.
|
||||
return;
|
||||
}
|
||||
MOZ_ASSERT(aEditor->IsEditActionDataAvailable());
|
||||
mEditor = aEditor;
|
||||
mEditor->PreserveSelectionAcrossActions();
|
||||
}
|
||||
|
||||
AutoSelectionRestorer::~AutoSelectionRestorer() {
|
||||
if (!mEditor || !mEditor->ArePreservingSelection()) {
|
||||
return;
|
||||
}
|
||||
DebugOnly<nsresult> rvIgnored = mEditor->RestorePreservedSelection();
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rvIgnored),
|
||||
"EditorBase::RestorePreservedSelection() failed, but ignored");
|
||||
}
|
||||
|
||||
void AutoSelectionRestorer::Abort() {
|
||||
if (mEditor) {
|
||||
mEditor->StopPreservingSelection();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
51
editor/libeditor/AutoSelectionRestorer.h
Normal file
51
editor/libeditor/AutoSelectionRestorer.h
Normal file
@ -0,0 +1,51 @@
|
||||
/* -*- 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/. */
|
||||
|
||||
#ifndef mozilla_AutoSelectionRestorer_h__
|
||||
#define mozilla_AutoSelectionRestorer_h__
|
||||
|
||||
#include "EditorBase.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
/**
|
||||
* Stack based helper class for saving/restoring selection. Note that this
|
||||
* assumes that the nodes involved are still around afterwords!
|
||||
*/
|
||||
class AutoSelectionRestorer final {
|
||||
public:
|
||||
AutoSelectionRestorer() = delete;
|
||||
explicit AutoSelectionRestorer(const AutoSelectionRestorer& aOther) = delete;
|
||||
AutoSelectionRestorer(AutoSelectionRestorer&& aOther) = delete;
|
||||
|
||||
/**
|
||||
* Constructor responsible for remembering all state needed to restore
|
||||
* aSelection.
|
||||
* XXX This constructor and the destructor should be marked as
|
||||
* `MOZ_CAN_RUN_SCRIPT`, but it's impossible due to this may be used
|
||||
* with `Maybe`.
|
||||
*/
|
||||
MOZ_CAN_RUN_SCRIPT_BOUNDARY explicit AutoSelectionRestorer(
|
||||
EditorBase* aEditor);
|
||||
|
||||
/**
|
||||
* Destructor restores mSelection to its former state
|
||||
*/
|
||||
MOZ_CAN_RUN_SCRIPT_BOUNDARY ~AutoSelectionRestorer();
|
||||
|
||||
/**
|
||||
* Abort() cancels to restore the selection.
|
||||
*/
|
||||
void Abort();
|
||||
|
||||
bool MaybeRestoreSelectionLater() const { return !!mEditor; }
|
||||
|
||||
protected:
|
||||
// The lifetime must be guaranteed by the creator of this instance.
|
||||
MOZ_KNOWN_LIVE EditorBase* mEditor = nullptr;
|
||||
};
|
||||
} // namespace mozilla
|
||||
|
||||
#endif
|
@ -9,6 +9,7 @@
|
||||
#include <string.h> // for strcmp
|
||||
|
||||
#include "AutoRangeArray.h" // for AutoRangeArray
|
||||
#include "AutoSelectionRestorer.h"
|
||||
#include "ChangeAttributeTransaction.h"
|
||||
#include "CompositionTransaction.h"
|
||||
#include "DeleteContentTransactionBase.h"
|
||||
@ -58,6 +59,7 @@
|
||||
#include "mozilla/Preferences.h" // for Preferences
|
||||
#include "mozilla/PresShell.h" // for PresShell
|
||||
#include "mozilla/RangeBoundary.h" // for RawRangeBoundary, RangeBoundary
|
||||
#include "mozilla/ScopeExit.h" // for MakeScopeExit
|
||||
#include "mozilla/Services.h" // for GetObserverService
|
||||
#include "mozilla/StaticPrefs_bidi.h" // for StaticPrefs::bidi_*
|
||||
#include "mozilla/StaticPrefs_dom.h" // for StaticPrefs::dom_*
|
||||
@ -5370,7 +5372,7 @@ nsresult EditorBase::OnInputText(const nsAString& aStringToInsert) {
|
||||
nsresult EditorBase::ReplaceTextAsAction(
|
||||
const nsAString& aString, nsRange* aReplaceRange,
|
||||
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
|
||||
nsIPrincipal* aPrincipal) {
|
||||
PreventSetSelection aPreventSetSelection, nsIPrincipal* aPrincipal) {
|
||||
MOZ_ASSERT(aString.FindChar(nsCRT::CR) == kNotFound);
|
||||
MOZ_ASSERT_IF(!aReplaceRange, IsTextEditor());
|
||||
|
||||
@ -5458,9 +5460,25 @@ nsresult EditorBase::ReplaceTextAsAction(
|
||||
// shouldn't receive such selectionchange before the first mutation.
|
||||
AutoUpdateViewBatch preventSelectionChangeEvent(*this, __FUNCTION__);
|
||||
|
||||
ErrorResult error;
|
||||
|
||||
AutoSelectionRestorer restorer(
|
||||
aPreventSetSelection == PreventSetSelection::Yes ? this : nullptr);
|
||||
|
||||
auto raii = MakeScopeExit([&] {
|
||||
if (aPreventSetSelection == PreventSetSelection::Yes) {
|
||||
if (error.Failed()) {
|
||||
restorer.Abort();
|
||||
return;
|
||||
}
|
||||
if (NS_FAILED(rv)) {
|
||||
restorer.Abort();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Select the range but as far as possible, we should not create new range
|
||||
// even if it's part of special Selection.
|
||||
ErrorResult error;
|
||||
SelectionRef().RemoveAllRanges(error);
|
||||
if (error.Failed()) {
|
||||
NS_WARNING("Selection::RemoveAllRanges() failed");
|
||||
@ -5476,6 +5494,7 @@ nsresult EditorBase::ReplaceTextAsAction(
|
||||
rv = ReplaceSelectionAsSubAction(aString);
|
||||
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
||||
"EditorBase::ReplaceSelectionAsSubAction() failed");
|
||||
|
||||
return EditorBase::ToGenericNSResult(rv);
|
||||
}
|
||||
|
||||
@ -7110,4 +7129,39 @@ nsISelectionController* EditorBase::GetSelectionController() const {
|
||||
return mDocument->GetPresShell();
|
||||
}
|
||||
|
||||
bool EditorBase::ArePreservingSelection() const {
|
||||
return IsEditActionDataAvailable() && SavedSelectionRef().RangeCount();
|
||||
}
|
||||
|
||||
void EditorBase::PreserveSelectionAcrossActions() {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
SavedSelectionRef().SaveSelection(SelectionRef());
|
||||
RangeUpdaterRef().RegisterSelectionState(SavedSelectionRef());
|
||||
}
|
||||
|
||||
nsresult EditorBase::RestorePreservedSelection() {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
if (!SavedSelectionRef().RangeCount()) {
|
||||
// XXX Returning error when it does not store is odd because no selection
|
||||
// ranges is not illegal case in general.
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
DebugOnly<nsresult> rvIgnored =
|
||||
SavedSelectionRef().RestoreSelection(SelectionRef());
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rvIgnored),
|
||||
"SelectionState::RestoreSelection() failed, but ignored");
|
||||
StopPreservingSelection();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void EditorBase::StopPreservingSelection() {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
RangeUpdaterRef().DropSelectionState(SavedSelectionRef());
|
||||
SavedSelectionRef().RemoveAllRanges();
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
@ -703,6 +703,11 @@ class EditorBase : public nsIEditor,
|
||||
Yes,
|
||||
};
|
||||
|
||||
enum class PreventSetSelection {
|
||||
No,
|
||||
Yes,
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace text in aReplaceRange or all text in this editor with aString and
|
||||
* treat the change as inserting the string.
|
||||
@ -715,6 +720,10 @@ class EditorBase : public nsIEditor,
|
||||
* @param aAllowBeforeInputEventCancelable
|
||||
* Whether `beforeinput` event which will be
|
||||
* dispatched for this can be cancelable or not.
|
||||
* @param aPreventSetSelection
|
||||
* Whether setting selection after replacing text.
|
||||
* If No, selection is the tail of replaced text.
|
||||
* If Yes, selection isn't changed.
|
||||
* @param aPrincipal Set subject principal if it may be called by
|
||||
* JS. If set to nullptr, will be treated as
|
||||
* called by system.
|
||||
@ -722,6 +731,7 @@ class EditorBase : public nsIEditor,
|
||||
MOZ_CAN_RUN_SCRIPT nsresult ReplaceTextAsAction(
|
||||
const nsAString& aString, nsRange* aReplaceRange,
|
||||
AllowBeforeInputEventCancelable aAllowBeforeInputEventCancelable,
|
||||
PreventSetSelection aPreventSetSelection = PreventSetSelection::No,
|
||||
nsIPrincipal* aPrincipal = nullptr);
|
||||
|
||||
/**
|
||||
@ -1396,7 +1406,7 @@ class EditorBase : public nsIEditor,
|
||||
// the DOM tree. In such case, we need to handle edit action separately.
|
||||
AutoEditActionDataSetter* mParentData;
|
||||
|
||||
// Cached selection for HTMLEditor::AutoSelectionRestorer.
|
||||
// Cached selection for AutoSelectionRestorer.
|
||||
SelectionState mSavedSelection;
|
||||
|
||||
// Utility class object for maintaining preserved ranges.
|
||||
@ -1600,15 +1610,13 @@ class EditorBase : public nsIEditor,
|
||||
|
||||
/**
|
||||
* SavedSelection() returns reference to saved selection which are
|
||||
* stored by HTMLEditor::AutoSelectionRestorer.
|
||||
* stored by AutoSelectionRestorer.
|
||||
*/
|
||||
SelectionState& SavedSelectionRef() {
|
||||
MOZ_ASSERT(IsHTMLEditor());
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
return mEditActionData->SavedSelectionRef();
|
||||
}
|
||||
const SelectionState& SavedSelectionRef() const {
|
||||
MOZ_ASSERT(IsHTMLEditor());
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
return mEditActionData->SavedSelectionRef();
|
||||
}
|
||||
@ -2168,6 +2176,15 @@ class EditorBase : public nsIEditor,
|
||||
enum class SafeToInsertData : bool { No, Yes };
|
||||
SafeToInsertData IsSafeToInsertData(nsIPrincipal* aSourcePrincipal) const;
|
||||
|
||||
/**
|
||||
* Routines for managing the preservation of selection across
|
||||
* various editor actions.
|
||||
*/
|
||||
bool ArePreservingSelection() const;
|
||||
void PreserveSelectionAcrossActions();
|
||||
MOZ_CAN_RUN_SCRIPT nsresult RestorePreservedSelection();
|
||||
void StopPreservingSelection();
|
||||
|
||||
protected: // Called by helper classes.
|
||||
/**
|
||||
* OnStartToHandleTopLevelEditSubAction() is called when
|
||||
@ -2954,8 +2971,9 @@ class EditorBase : public nsIEditor,
|
||||
friend class AlignStateAtSelection; // AutoEditActionDataSetter,
|
||||
// ToGenericNSResult
|
||||
friend class AutoRangeArray; // IsSEditActionDataAvailable, SelectionRef
|
||||
friend class CaretPoint; // AllowsTransactionsToChangeSelection,
|
||||
// CollapseSelectionTo
|
||||
friend class AutoSelectionRestorer; // RangeUpdaterRef, SavedSelectionRef
|
||||
friend class CaretPoint; // AllowsTransactionsToChangeSelection,
|
||||
// CollapseSelectionTo
|
||||
friend class CompositionTransaction; // CollapseSelectionTo, DoDeleteText,
|
||||
// DoInsertText, DoReplaceText,
|
||||
// HideCaret, RangeupdaterRef
|
||||
|
@ -86,6 +86,7 @@ class SelectionState; // mozilla/SelectionState.h
|
||||
class TextEditor; // mozilla/TextEditor.h
|
||||
|
||||
class AutoRangeArray; // AutoRangeArray.h
|
||||
class AutoSelectionRestorer; // AutoSelectionRestorer.h
|
||||
class AutoSelectionRangeArray; // EditorUtils.h
|
||||
class CaretPoint; // EditorUtils.h
|
||||
class ChangeAttributeTransaction; // ChangeAttributeTransaction.h
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "AutoRangeArray.h"
|
||||
#include "AutoSelectionRestorer.h"
|
||||
#include "CSSEditUtils.h"
|
||||
#include "EditAction.h"
|
||||
#include "EditorDOMPoint.h"
|
||||
@ -568,7 +569,7 @@ nsresult HTMLEditor::OnEndHandlingTopLevelEditSubActionInternal() {
|
||||
// mutation by themselves. However, we want to keep selection as-is.
|
||||
// Therefore, we should restore `Selection` after replacing
|
||||
// white-spaces.
|
||||
AutoSelectionRestorer restoreSelection(*this);
|
||||
AutoSelectionRestorer restoreSelection(this);
|
||||
// TODO: Temporarily, WhiteSpaceVisibilityKeeper replaces ASCII
|
||||
// white-spaces with NPSPs and then, we'll replace them with ASCII
|
||||
// white-spaces here. We should avoid this overwriting things as
|
||||
@ -4565,7 +4566,7 @@ nsresult HTMLEditor::RemoveListAtSelectionAsSubAction(
|
||||
}
|
||||
}
|
||||
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
AutoTArray<OwningNonNull<nsIContent>, 64> arrayOfContents;
|
||||
{
|
||||
@ -6044,7 +6045,7 @@ Result<SplitRangeOffFromNodeResult, nsresult>
|
||||
HTMLEditor::HandleOutdentAtSelectionInternal(const Element& aEditingHost) {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
bool useCSS = IsCSSEnabled();
|
||||
|
||||
@ -11533,7 +11534,7 @@ nsresult HTMLEditor::MoveSelectedContentsToDivElementToMakeItAbsolutePosition(
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
MOZ_ASSERT(aTargetElement);
|
||||
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
EditorDOMPoint pointToPutCaret;
|
||||
|
||||
@ -12005,7 +12006,7 @@ HTMLEditor::SetSelectionToStaticAsSubAction() {
|
||||
}
|
||||
|
||||
{
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
nsresult rv = SetPositionToAbsoluteOrStatic(*element, false);
|
||||
if (NS_WARN_IF(Destroyed())) {
|
||||
@ -12101,7 +12102,7 @@ Result<EditActionResult, nsresult> HTMLEditor::AddZIndexAsSubAction(
|
||||
}
|
||||
|
||||
{
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
// MOZ_KnownLive(*absolutelyPositionedStyledElement): It's
|
||||
// absolutelyPositionedElement whose type is RefPtr.
|
||||
|
@ -117,39 +117,6 @@ struct MOZ_STACK_CLASS SavedRange final {
|
||||
uint32_t mEndOffset = 0;
|
||||
};
|
||||
|
||||
/******************************************************************************
|
||||
* HTMLEditor::AutoSelectionRestorer
|
||||
*****************************************************************************/
|
||||
|
||||
HTMLEditor::AutoSelectionRestorer::AutoSelectionRestorer(
|
||||
HTMLEditor& aHTMLEditor)
|
||||
: mHTMLEditor(nullptr) {
|
||||
if (aHTMLEditor.ArePreservingSelection()) {
|
||||
// We already have initialized mParentData::mSavedSelection, so this must
|
||||
// be nested call.
|
||||
return;
|
||||
}
|
||||
MOZ_ASSERT(aHTMLEditor.IsEditActionDataAvailable());
|
||||
mHTMLEditor = &aHTMLEditor;
|
||||
mHTMLEditor->PreserveSelectionAcrossActions();
|
||||
}
|
||||
|
||||
HTMLEditor::AutoSelectionRestorer::~AutoSelectionRestorer() {
|
||||
if (!mHTMLEditor || !mHTMLEditor->ArePreservingSelection()) {
|
||||
return;
|
||||
}
|
||||
DebugOnly<nsresult> rvIgnored = mHTMLEditor->RestorePreservedSelection();
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rvIgnored),
|
||||
"EditorBase::RestorePreservedSelection() failed, but ignored");
|
||||
}
|
||||
|
||||
void HTMLEditor::AutoSelectionRestorer::Abort() {
|
||||
if (mHTMLEditor) {
|
||||
mHTMLEditor->StopPreservingSelection();
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* HTMLEditor
|
||||
*****************************************************************************/
|
||||
@ -1280,41 +1247,6 @@ nsresult HTMLEditor::MaybeCollapseSelectionAtFirstEditableNode(
|
||||
return rv;
|
||||
}
|
||||
|
||||
bool HTMLEditor::ArePreservingSelection() const {
|
||||
return IsEditActionDataAvailable() && SavedSelectionRef().RangeCount();
|
||||
}
|
||||
|
||||
void HTMLEditor::PreserveSelectionAcrossActions() {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
SavedSelectionRef().SaveSelection(SelectionRef());
|
||||
RangeUpdaterRef().RegisterSelectionState(SavedSelectionRef());
|
||||
}
|
||||
|
||||
nsresult HTMLEditor::RestorePreservedSelection() {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
if (!SavedSelectionRef().RangeCount()) {
|
||||
// XXX Returning error when it does not store is odd because no selection
|
||||
// ranges is not illegal case in general.
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
DebugOnly<nsresult> rvIgnored =
|
||||
SavedSelectionRef().RestoreSelection(SelectionRef());
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rvIgnored),
|
||||
"SelectionState::RestoreSelection() failed, but ignored");
|
||||
StopPreservingSelection();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void HTMLEditor::StopPreservingSelection() {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
RangeUpdaterRef().DropSelectionState(SavedSelectionRef());
|
||||
SavedSelectionRef().RemoveAllRanges();
|
||||
}
|
||||
|
||||
void HTMLEditor::PreHandleMouseDown(const MouseEvent& aMouseDownEvent) {
|
||||
if (mPendingStylesToApplyToNewContent) {
|
||||
// mPendingStylesToApplyToNewContent will be notified of selection change
|
||||
|
@ -1016,15 +1016,6 @@ class HTMLEditor final : public EditorBase,
|
||||
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult
|
||||
DoJoinNodes(nsIContent& aContentToKeep, nsIContent& aContentToRemove);
|
||||
|
||||
/**
|
||||
* Routines for managing the preservation of selection across
|
||||
* various editor actions.
|
||||
*/
|
||||
bool ArePreservingSelection() const;
|
||||
void PreserveSelectionAcrossActions();
|
||||
MOZ_CAN_RUN_SCRIPT nsresult RestorePreservedSelection();
|
||||
void StopPreservingSelection();
|
||||
|
||||
/**
|
||||
* Called when JoinNodesTransaction::DoTransaction() did its transaction.
|
||||
* Note that this is not called when undoing nor redoing.
|
||||
@ -4337,44 +4328,6 @@ class HTMLEditor final : public EditorBase,
|
||||
nsIContent& aContentMovingToSubList,
|
||||
const Element& aEditingHost);
|
||||
|
||||
/**
|
||||
* Stack based helper class for saving/restoring selection. Note that this
|
||||
* assumes that the nodes involved are still around afterwords!
|
||||
*/
|
||||
class AutoSelectionRestorer final {
|
||||
public:
|
||||
AutoSelectionRestorer() = delete;
|
||||
explicit AutoSelectionRestorer(const AutoSelectionRestorer& aOther) =
|
||||
delete;
|
||||
AutoSelectionRestorer(AutoSelectionRestorer&& aOther) = delete;
|
||||
|
||||
/**
|
||||
* Constructor responsible for remembering all state needed to restore
|
||||
* aSelection.
|
||||
* XXX This constructor and the destructor should be marked as
|
||||
* `MOZ_CAN_RUN_SCRIPT`, but it's impossible due to this may be used
|
||||
* with `Maybe`.
|
||||
*/
|
||||
MOZ_CAN_RUN_SCRIPT_BOUNDARY explicit AutoSelectionRestorer(
|
||||
HTMLEditor& aHTMLEditor);
|
||||
|
||||
/**
|
||||
* Destructor restores mSelection to its former state
|
||||
*/
|
||||
MOZ_CAN_RUN_SCRIPT_BOUNDARY ~AutoSelectionRestorer();
|
||||
|
||||
/**
|
||||
* Abort() cancels to restore the selection.
|
||||
*/
|
||||
void Abort();
|
||||
|
||||
bool MaybeRestoreSelectionLater() const { return !!mHTMLEditor; }
|
||||
|
||||
protected:
|
||||
// The lifetime must be guaranteed by the creator of this instance.
|
||||
MOZ_KNOWN_LIVE HTMLEditor* mHTMLEditor = nullptr;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stack based helper class for calling EditorBase::EndTransactionInternal().
|
||||
* NOTE: This does not suppress multiple input events. In most cases,
|
||||
@ -4524,6 +4477,7 @@ class HTMLEditor final : public EditorBase,
|
||||
// CollectNonEditableNodes
|
||||
friend class AutoRangeArray; // RangeUpdaterRef, SplitNodeWithTransaction,
|
||||
// SplitInlineAncestorsAtRangeBoundaries
|
||||
friend class AutoSelectionRestore;
|
||||
friend class AutoSelectionSetterAfterTableEdit; // SetSelectionAfterEdit
|
||||
friend class
|
||||
AutoSetTemporaryAncestorLimiter; // InitializeSelectionAncestorLimit
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "AutoSelectionRestorer.h"
|
||||
#include "EditAction.h"
|
||||
#include "EditorDOMPoint.h"
|
||||
#include "EditorUtils.h"
|
||||
@ -703,7 +704,7 @@ Result<EditActionResult, nsresult> HTMLEditor::HTMLWithContextInserter::Run(
|
||||
// Save current selection since DeleteTableCellWithTransaction() perturbs
|
||||
// it.
|
||||
{
|
||||
AutoSelectionRestorer restoreSelectionLater(mHTMLEditor);
|
||||
AutoSelectionRestorer restoreSelectionLater(&mHTMLEditor);
|
||||
rv = mHTMLEditor.DeleteTableCellWithTransaction(1);
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("HTMLEditor::DeleteTableCellWithTransaction(1) failed");
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include "HTMLEditor.h"
|
||||
#include "HTMLEditorInlines.h"
|
||||
|
||||
#include "AutoSelectionRestorer.h"
|
||||
#include "EditAction.h"
|
||||
#include "EditorDOMPoint.h"
|
||||
#include "EditorUtils.h"
|
||||
@ -2913,7 +2914,7 @@ NS_IMETHODIMP HTMLEditor::SwitchTableCellHeaderType(Element* aSourceCell,
|
||||
// Save current selection to restore when done.
|
||||
// This is needed so ReplaceContainerAndCloneAttributesWithTransaction()
|
||||
// can monitor selection when replacing nodes.
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
// Set to the opposite of current type
|
||||
nsAtom* newCellName =
|
||||
@ -3636,7 +3637,7 @@ nsresult HTMLEditor::NormalizeTableInternal(Element& aTableOrElementInTable) {
|
||||
TableSize tableSize = tableSizeOrError.unwrap();
|
||||
|
||||
// Save current selection
|
||||
AutoSelectionRestorer restoreSelectionLater(*this);
|
||||
AutoSelectionRestorer restoreSelectionLater(this);
|
||||
|
||||
AutoPlaceholderBatch treateAsOneTransaction(
|
||||
*this, ScrollSelectionIntoView::Yes, __FUNCTION__);
|
||||
|
@ -31,6 +31,7 @@ EXPORTS.mozilla += [
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
"AutoRangeArray.cpp",
|
||||
"AutoSelectionRestorer.cpp",
|
||||
"ChangeAttributeTransaction.cpp",
|
||||
"ChangeStyleTransaction.cpp",
|
||||
"CompositionTransaction.cpp",
|
||||
|
@ -590,6 +590,8 @@ skip-if = ["headless"] # The test calls `synthesizeKey`, see bug 1669923.
|
||||
|
||||
["test_pasting_text_longer_than_maxlength.html"]
|
||||
|
||||
["test_replace_text.html"]
|
||||
|
||||
["test_resizers_appearance.html"]
|
||||
|
||||
["test_resizers_resizing_elements.html"]
|
||||
|
400
editor/libeditor/tests/test_replace_text.html
Normal file
400
editor/libeditor/tests/test_replace_text.html
Normal file
@ -0,0 +1,400 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=1149826
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>Test for replaceText</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1149826">Mozilla Bug 1149826</a><br>
|
||||
<input type="text"><br>
|
||||
<textarea></textarea>
|
||||
<div contenteditable></div>
|
||||
|
||||
<script>
|
||||
const gDOMWindowUtils = _getDOMWindowUtils(window);
|
||||
const Ci = SpecialPowers.Ci;
|
||||
const IS_WIN = navigator.platform.indexOf("Win") == 0;
|
||||
|
||||
async function testReplaceText(INPUT_TESTS, TEXTAREA_TESTS, CONTENTEDITABLE_TESTS, aPreventSetSelection) {
|
||||
await SimpleTest.promiseFocus();
|
||||
|
||||
const flags = aPreventSetSelection ?
|
||||
Ci.nsIDOMWindowUtils.CONTENT_COMMAND_FLAG_PREVENT_SET_SELECTION :
|
||||
0;
|
||||
|
||||
const input = document.querySelector("input");
|
||||
input.focus();
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
info("for <input>");
|
||||
|
||||
for (const TEST of INPUT_TESTS) {
|
||||
input.value = TEST.before.value;
|
||||
input.selectionStart = TEST.before.start;
|
||||
input.selectionEnd = TEST.before.end;
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
input.addEventListener("beforeinput", e => {
|
||||
is(e.inputType, "insertReplacementText",
|
||||
"inputType in input must be insertReplacementText by replaceText");
|
||||
is(input.selectionStart, TEST.before.start,
|
||||
"Before inputReplacementText, start offset is valid");
|
||||
is(input.selectionEnd, TEST.before.end,
|
||||
"Before inputReplacementText, end offset is valid");
|
||||
}, { once: true } );
|
||||
|
||||
const promiseAfterOnInput =
|
||||
new Promise(resolve => input.addEventListener("input", e => {
|
||||
is(e.inputType, "insertReplacementText",
|
||||
"inputType must be insertReplacementText by replaceText");
|
||||
resolve();
|
||||
}, { once: true } ));
|
||||
gDOMWindowUtils.sendContentCommandEvent(
|
||||
"replaceText",
|
||||
null,
|
||||
TEST.replace.value,
|
||||
TEST.replace.start,
|
||||
TEST.replace.src,
|
||||
flags
|
||||
);
|
||||
await promiseAfterOnInput
|
||||
|
||||
is(input.value, TEST.after.value,
|
||||
"replaceText in input replaces inner text");
|
||||
is(input.selectionStart, TEST.after.start,
|
||||
"replaceText in input sets expected selection start");
|
||||
is(input.selectionEnd, TEST.after.end,
|
||||
"replaceText in input sets expected selection end");
|
||||
}
|
||||
|
||||
const textarea = document.querySelector("textarea");
|
||||
textarea.focus();
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
info("for <textarea>");
|
||||
|
||||
for (const TEST of TEXTAREA_TESTS) {
|
||||
textarea.value = TEST.before.value;
|
||||
textarea.selectionStart = TEST.before.start;
|
||||
textarea.selectionEnd = TEST.before.end;
|
||||
|
||||
textarea.addEventListener("beforeinput", e => {
|
||||
is(e.inputType, "insertReplacementText",
|
||||
"inputType must be insertReplacementText by replaceText");
|
||||
is(textarea.selectionStart, TEST.before.start,
|
||||
"Before inputReplacementText, start offset is valid");
|
||||
is(textarea.selectionEnd, TEST.before.end,
|
||||
"Before inputReplacementText, end offset is valid");
|
||||
}, { once: true } );
|
||||
|
||||
const promiseAfterOnTextarea =
|
||||
new Promise(resolve => textarea.addEventListener("input", e => {
|
||||
is(e.inputType, "insertReplacementText",
|
||||
"inputType must be insertReplacementText by replaceText");
|
||||
resolve();
|
||||
}, { once: true } ));
|
||||
gDOMWindowUtils.sendContentCommandEvent(
|
||||
"replaceText",
|
||||
null,
|
||||
TEST.replace.value,
|
||||
TEST.replace.start,
|
||||
TEST.replace.src,
|
||||
flags
|
||||
);
|
||||
await promiseAfterOnTextarea
|
||||
|
||||
is(textarea.value, TEST.after.value,
|
||||
"replaceText in textarea replaces inner text");
|
||||
is(textarea.selectionStart, TEST.after.start,
|
||||
"replaceText in textarea sets expected selection start");
|
||||
is(textarea.selectionEnd, TEST.after.end,
|
||||
"replaceText in textarea sets expected selection end");
|
||||
}
|
||||
|
||||
const editingHost = document.querySelector("div[contenteditable]");
|
||||
editingHost.focus();
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
info("for contenteditable");
|
||||
|
||||
for (const TEST of CONTENTEDITABLE_TESTS) {
|
||||
editingHost.innerHTML = TEST.before.value;
|
||||
window.getSelection().setBaseAndExtent(
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(TEST.before.focusNode),
|
||||
TEST.before.focusOffset,
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(TEST.before.focusNode),
|
||||
TEST.before.focusOffset
|
||||
);
|
||||
|
||||
editingHost.addEventListener("beforeinput", e => {
|
||||
const selection = window.getSelection();
|
||||
is(e.inputType, "insertReplacementText",
|
||||
"inputType must be insertReplacementText by replaceText");
|
||||
// eslint-disable-next-line no-eval
|
||||
is(selection.focusNode, eval(TEST.before.focusNode),
|
||||
"Before inputReplacementText, focus node is valid");
|
||||
is(selection.focusOffset, TEST.before.focusOffset,
|
||||
"Before inputReplacementText, focus offset is valid");
|
||||
}, { once: true } );
|
||||
|
||||
const promiseAfterEditingHost =
|
||||
new Promise(resolve => editingHost.addEventListener("input", e => {
|
||||
is(e.inputType, "insertReplacementText",
|
||||
"inputType must be insertReplacementText by replaceText");
|
||||
resolve();
|
||||
}, { once: true } ));
|
||||
gDOMWindowUtils.sendContentCommandEvent(
|
||||
"replaceText",
|
||||
null,
|
||||
TEST.replace.value,
|
||||
TEST.replace.start,
|
||||
TEST.replace.src,
|
||||
flags
|
||||
);
|
||||
await promiseAfterEditingHost
|
||||
|
||||
is(editingHost.textContent, TEST.after.value,
|
||||
"replaceText in contenteditable replaces inner text");
|
||||
const selection = window.getSelection();
|
||||
// eslint-disable-next-line no-eval
|
||||
is(selection.focusNode, eval(TEST.after.focusNode),
|
||||
"replaceText in contenteditable sets expected focusNode");
|
||||
is(selection.focusOffset, TEST.after.focusOffset,
|
||||
"replaceText in contenteditable sets expected focusOffset");
|
||||
}
|
||||
}
|
||||
|
||||
add_task(async function testReplaceTextWithoutPreventSetSelection() {
|
||||
const INPUT_TESTS = [
|
||||
{ before: {
|
||||
value: "foo", start: 3, end: 3
|
||||
},
|
||||
replace: {
|
||||
src: "o", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbaro", start: 4, end: 4
|
||||
}
|
||||
},
|
||||
{ before: {
|
||||
value: "foo ", start: 4, end: 4
|
||||
},
|
||||
replace: {
|
||||
src: "oo", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbar ", start: 4, end: 4
|
||||
}
|
||||
}];
|
||||
|
||||
const TEXTAREA_TESTS = [
|
||||
{ before: {
|
||||
value: "foo", start: 3, end: 3
|
||||
},
|
||||
replace: {
|
||||
src: "o", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbaro", start: 4, end: 4
|
||||
}
|
||||
},
|
||||
{ before: {
|
||||
value: "foo ", start: 4, end: 4
|
||||
},
|
||||
replace: {
|
||||
src: "oo", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbar ", start: 4, end: 4
|
||||
}
|
||||
}];
|
||||
|
||||
const CONTENTEDITABLE_TESTS = [
|
||||
{ before: {
|
||||
value: "foo", focusNode: "editingHost.firstChild", focusOffset: 3
|
||||
},
|
||||
replace: {
|
||||
src: "o", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbaro", focusNode: "editingHost.firstChild", focusOffset: 4, isCollapsed: true
|
||||
},
|
||||
},
|
||||
{ before: {
|
||||
value: "foo foo", focusNode: "editingHost.firstChild", focusOffset: 4
|
||||
},
|
||||
replace: {
|
||||
src: "oo", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbar foo", focusNode: "editingHost.firstChild", focusOffset: 4, isCollapsed: true
|
||||
},
|
||||
}];
|
||||
|
||||
await testReplaceText(INPUT_TESTS, TEXTAREA_TESTS, CONTENTEDITABLE_TESTS, false);
|
||||
});
|
||||
|
||||
add_task(async function testReplaceTextWithPreventSetSelection() {
|
||||
const INPUT_TESTS = [
|
||||
{ before: {
|
||||
value: "foo", start: 3, end: 3
|
||||
},
|
||||
replace: {
|
||||
src: "o", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbaro", start: 5, end: 5
|
||||
}
|
||||
},
|
||||
{ before: {
|
||||
value: "foo ", start: 4, end: 4
|
||||
},
|
||||
replace: {
|
||||
src: "oo", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbar ", start: 5, end: 5
|
||||
}
|
||||
}];
|
||||
|
||||
const TEXTAREA_TESTS = [
|
||||
{ before: {
|
||||
value: "foo", start: 3, end: 3
|
||||
},
|
||||
replace: {
|
||||
src: "o", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbaro", start: 5, end: 5
|
||||
}
|
||||
},
|
||||
{ before: {
|
||||
value: "foo ", start: 4, end: 4
|
||||
},
|
||||
replace: {
|
||||
src: "oo", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbar ", start: 5, end: 5
|
||||
}
|
||||
}];
|
||||
|
||||
const CONTENTEDITABLE_TESTS = [
|
||||
{ before: {
|
||||
value: "foo", focusNode: "editingHost.firstChild", focusOffset: 3
|
||||
},
|
||||
replace: {
|
||||
src: "o", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbaro", focusNode: "editingHost.firstChild", focusOffset: 5, isCollapsed: true
|
||||
},
|
||||
},
|
||||
{ before: {
|
||||
value: "foo foo", focusNode: "editingHost.firstChild", focusOffset: 4
|
||||
},
|
||||
replace: {
|
||||
src: "oo", value: "bar", start: 1
|
||||
},
|
||||
after: {
|
||||
value: "fbar foo", focusNode: "editingHost.firstChild", focusOffset: 5, isCollapsed: true
|
||||
},
|
||||
}];
|
||||
|
||||
await testReplaceText(INPUT_TESTS, TEXTAREA_TESTS, CONTENTEDITABLE_TESTS, true);
|
||||
});
|
||||
|
||||
add_task(async function testReplaceTextWithCompositionText() {
|
||||
await SimpleTest.promiseFocus();
|
||||
|
||||
// Don't replace text during composition
|
||||
const input = document.querySelector("input");
|
||||
input.value = "";
|
||||
input.focus();
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
let promise =
|
||||
new Promise(resolve => input.addEventListener("compositionupdate", resolve, { once: true }));
|
||||
synthesizeCompositionChange(
|
||||
{ "composition":
|
||||
{ "string": "foo",
|
||||
"clauses":
|
||||
[
|
||||
{ "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE }
|
||||
]
|
||||
},
|
||||
});
|
||||
await promise;
|
||||
|
||||
input.addEventListener("input", e => {
|
||||
isnot(e.inputType, "insertReplacementText",
|
||||
"Don't fire insertReplacementText input event by replaceText");
|
||||
}, { once: true } );
|
||||
gDOMWindowUtils.sendContentCommandEvent("replaceText", null, "bar", 1, "o");
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
promise = new Promise(resolve => input.addEventListener("compositionend", resolve, { once: true }));
|
||||
synthesizeComposition({type: "compositioncommitasis", key: {key: "KEY_Enter"}});
|
||||
await promise;
|
||||
|
||||
is(input.value, "foo",
|
||||
"replaceText doesn't replace inner text when having composition");
|
||||
is(input.selectionStart, 3, "replaceText sets caret position to next of replaced text");
|
||||
is(input.selectionStart, input.selectionEnd, "replaceText sets that selection is collapsed");
|
||||
});
|
||||
|
||||
add_task(async function testReplaceTextBeforeCallingPreventDefault() {
|
||||
await SimpleTest.promiseFocus();
|
||||
|
||||
// Call preventDefault on beforeinput
|
||||
const input = document.querySelector("input");
|
||||
input.value = "foo";
|
||||
input.focus();
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
input.selectionStart = 1
|
||||
input.selectionEnd = 2;
|
||||
|
||||
const promise = new Promise(resolve => input.addEventListener("beforeinput", e => {
|
||||
e.preventDefault();
|
||||
resolve();
|
||||
}, { once: true }));
|
||||
gDOMWindowUtils.sendContentCommandEvent("replaceText", null, "bar", 1, "o");
|
||||
await promise;
|
||||
|
||||
is(input.value, "foo",
|
||||
"replaceText doesn't replace inner text of <input> since preventDefault is called");
|
||||
is(input.selectionStart, 1, "selectionStart isn't changed since preventDefault is called");
|
||||
is(input.selectionEnd, 2, "selectionEnd isn't changed since preventDefault is called");
|
||||
});
|
||||
|
||||
add_task(async function testReplaceTextWithoutMatch() {
|
||||
await SimpleTest.promiseFocus();
|
||||
|
||||
const input = document.querySelector("input");
|
||||
input.value = "foo";
|
||||
input.focus();
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
input.selectionStart = 1
|
||||
input.selectionEnd = 2;
|
||||
|
||||
gDOMWindowUtils.sendContentCommandEvent("replaceText", null, "bar", 1, "a");
|
||||
await new Promise(resolve => SimpleTest.executeSoon(resolve));
|
||||
|
||||
is(input.value, "foo",
|
||||
"replaceText doesn't replace inner text of <input> due to not matched");
|
||||
is(input.selectionStart, 1, "selectionStart isn't changed due to failed");
|
||||
is(input.selectionEnd, 2, "selectionEnd isn't changed due to failed");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -320,6 +320,7 @@ NS_EVENT_MESSAGE(eContentCommandRedo)
|
||||
// eContentCommandInsertText tries to insert text with replacing selection
|
||||
// in focused editor.
|
||||
NS_EVENT_MESSAGE(eContentCommandInsertText)
|
||||
NS_EVENT_MESSAGE(eContentCommandReplaceText)
|
||||
NS_EVENT_MESSAGE(eContentCommandPasteTransferable)
|
||||
NS_EVENT_MESSAGE(eContentCommandLookUpDictionary)
|
||||
// eContentCommandScroll scrolls the nearest scrollable element to the
|
||||
|
@ -49,7 +49,7 @@ class WidgetContentCommandEvent : public WidgetGUIEvent {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// eContentCommandInsertText
|
||||
// eContentCommandInsertText and eContentCommandReplaceText
|
||||
mozilla::Maybe<nsString> mString; // [in]
|
||||
|
||||
// eContentCommandPasteTransferable
|
||||
@ -68,6 +68,16 @@ class WidgetContentCommandEvent : public WidgetGUIEvent {
|
||||
bool mIsHorizontal; // [in]
|
||||
} mScroll;
|
||||
|
||||
// eContentCommandReplaceText
|
||||
struct Selection {
|
||||
// Replacement source string. If not matched, failed
|
||||
nsString mReplaceSrcString; // [in]
|
||||
// Start offset of selection
|
||||
uint32_t mOffset = 0; // [in]
|
||||
// false if selection is end of replaced string
|
||||
bool mPreventSetSelection = false; // [in]
|
||||
} mSelection;
|
||||
|
||||
bool mOnlyEnabledCheck; // [in]
|
||||
|
||||
bool mSucceeded; // [out]
|
||||
@ -79,6 +89,7 @@ class WidgetContentCommandEvent : public WidgetGUIEvent {
|
||||
|
||||
mString = aEvent.mString;
|
||||
mScroll = aEvent.mScroll;
|
||||
mSelection = aEvent.mSelection;
|
||||
mOnlyEnabledCheck = aEvent.mOnlyEnabledCheck;
|
||||
mSucceeded = aEvent.mSucceeded;
|
||||
mIsEnabled = aEvent.mIsEnabled;
|
||||
|
Loading…
Reference in New Issue
Block a user