зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1658702 - part 21 Initialize target ranges for all edit actions which runs `DeleteSelectionAsSubAction()` r=m_kato
A lot of edit actions calls `DeleteSelectionAsSubAction()` if selection is not collapsed. In such case, `getTargetRanges()` should return same result as when the selection range is simply deleted. This patch creates 2 methods to consider whether `EditAction` causes running `DeleteSelectionAsSubAction()` with collapsed selection or non-collapsed selection. And makes `DeleteSelectionAsAction()` stop initializing the target ranges itself. Instead, makes `AutoEditActionDataSetter` do it immediately before dispatching `beforeinput` event unless it's been already initialized manually. * https://searchfox.org/mozilla-central/rev/30e70f2fe80c97bfbfcd975e68538cefd7f58b2a/editor/libeditor/TextEditor.cpp#492 * https://searchfox.org/mozilla-central/rev/30e70f2fe80c97bfbfcd975e68538cefd7f58b2a/editor/libeditor/TextEditor.cpp#731 * https://searchfox.org/mozilla-central/rev/30e70f2fe80c97bfbfcd975e68538cefd7f58b2a/editor/libeditor/TextEditorDataTransfer.cpp#503 The correctness of the new utility methods are tested with new `MOZ_ASSERT` in `DeleteSelectionAsSubAction()`. Additionally, this reorganizes `input-events-get-target-ranges-*.html`. * Moving common code into `input-events-get-target-ranges.js` * Moving non-collapsed selection cases into `input-events-get-target-ranges-non-collapsed-selection.html` * Adding "typing a" case into the new test for testing this patch's behavior Depends on D90542 Differential Revision: https://phabricator.services.mozilla.com/D90639
This commit is contained in:
Родитель
8d848ca717
Коммит
d3fe79f0e9
|
@ -593,6 +593,190 @@ inline EditorInputType ToInputType(EditAction aEditAction) {
|
|||
}
|
||||
}
|
||||
|
||||
inline bool MayEditActionDeleteAroundCollapsedSelection(
|
||||
const EditAction aEditAction) {
|
||||
switch (aEditAction) {
|
||||
case EditAction::eDeleteSelection:
|
||||
case EditAction::eDeleteBackward:
|
||||
case EditAction::eDeleteForward:
|
||||
case EditAction::eDeleteWordBackward:
|
||||
case EditAction::eDeleteWordForward:
|
||||
case EditAction::eDeleteToBeginningOfSoftLine:
|
||||
case EditAction::eDeleteToEndOfSoftLine:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool IsEditActionTableEditing(const EditAction aEditAction) {
|
||||
switch (aEditAction) {
|
||||
case EditAction::eInsertTableRowElement:
|
||||
case EditAction::eRemoveTableRowElement:
|
||||
case EditAction::eInsertTableColumn:
|
||||
case EditAction::eRemoveTableColumn:
|
||||
case EditAction::eRemoveTableElement:
|
||||
case EditAction::eRemoveTableCellElement:
|
||||
case EditAction::eDeleteTableCellContents:
|
||||
case EditAction::eInsertTableCellElement:
|
||||
case EditAction::eJoinTableCellElements:
|
||||
case EditAction::eSplitTableCellElement:
|
||||
case EditAction::eSetTableCellElementType:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool MayEditActionDeleteSelection(const EditAction aEditAction) {
|
||||
switch (aEditAction) {
|
||||
case EditAction::eNone:
|
||||
case EditAction::eNotEditing:
|
||||
return false;
|
||||
|
||||
// EditActions modifying around selection.
|
||||
case EditAction::eInsertText:
|
||||
case EditAction::eInsertParagraphSeparator:
|
||||
case EditAction::eInsertLineBreak:
|
||||
case EditAction::eDeleteSelection:
|
||||
case EditAction::eDeleteBackward:
|
||||
case EditAction::eDeleteForward:
|
||||
case EditAction::eDeleteWordBackward:
|
||||
case EditAction::eDeleteWordForward:
|
||||
case EditAction::eDeleteToBeginningOfSoftLine:
|
||||
case EditAction::eDeleteToEndOfSoftLine:
|
||||
case EditAction::eDeleteByDrag:
|
||||
return true;
|
||||
|
||||
case EditAction::eStartComposition:
|
||||
return false;
|
||||
|
||||
case EditAction::eUpdateComposition:
|
||||
case EditAction::eCommitComposition:
|
||||
case EditAction::eCancelComposition:
|
||||
case EditAction::eDeleteByComposition:
|
||||
return true;
|
||||
|
||||
case EditAction::eUndo:
|
||||
case EditAction::eRedo:
|
||||
case EditAction::eSetTextDirection:
|
||||
return false;
|
||||
|
||||
case EditAction::eCut:
|
||||
return true;
|
||||
|
||||
case EditAction::eCopy:
|
||||
return false;
|
||||
|
||||
case EditAction::ePaste:
|
||||
case EditAction::ePasteAsQuotation:
|
||||
return true;
|
||||
|
||||
case EditAction::eDrop:
|
||||
return false; // Not deleting selection at drop.
|
||||
|
||||
// EditActions changing format around selection.
|
||||
case EditAction::eIndent:
|
||||
case EditAction::eOutdent:
|
||||
return false;
|
||||
|
||||
// EditActions inserting or deleting something at specified position.
|
||||
case EditAction::eInsertTableRowElement:
|
||||
case EditAction::eRemoveTableRowElement:
|
||||
case EditAction::eInsertTableColumn:
|
||||
case EditAction::eRemoveTableColumn:
|
||||
case EditAction::eResizingElement:
|
||||
case EditAction::eResizeElement:
|
||||
case EditAction::eMovingElement:
|
||||
case EditAction::eMoveElement:
|
||||
case EditAction::eUnknown:
|
||||
case EditAction::eSetAttribute:
|
||||
case EditAction::eRemoveAttribute:
|
||||
case EditAction::eRemoveNode:
|
||||
case EditAction::eInsertBlockElement:
|
||||
return false;
|
||||
|
||||
// EditActions inserting someting around selection or replacing selection
|
||||
// with something.
|
||||
case EditAction::eReplaceText:
|
||||
case EditAction::eInsertNode:
|
||||
case EditAction::eInsertHorizontalRuleElement:
|
||||
return true;
|
||||
|
||||
// EditActions chaning format around selection or inserting or deleting
|
||||
// something at specific position.
|
||||
case EditAction::eInsertLinkElement:
|
||||
case EditAction::eInsertUnorderedListElement:
|
||||
case EditAction::eInsertOrderedListElement:
|
||||
case EditAction::eRemoveUnorderedListElement:
|
||||
case EditAction::eRemoveOrderedListElement:
|
||||
case EditAction::eRemoveListElement:
|
||||
case EditAction::eInsertBlockquoteElement:
|
||||
case EditAction::eNormalizeTable:
|
||||
case EditAction::eRemoveTableElement:
|
||||
case EditAction::eRemoveTableCellElement:
|
||||
case EditAction::eDeleteTableCellContents:
|
||||
case EditAction::eInsertTableCellElement:
|
||||
case EditAction::eJoinTableCellElements:
|
||||
case EditAction::eSplitTableCellElement:
|
||||
case EditAction::eSetTableCellElementType:
|
||||
case EditAction::eSetInlineStyleProperty:
|
||||
case EditAction::eRemoveInlineStyleProperty:
|
||||
case EditAction::eSetFontWeightProperty:
|
||||
case EditAction::eRemoveFontWeightProperty:
|
||||
case EditAction::eSetTextStyleProperty:
|
||||
case EditAction::eRemoveTextStyleProperty:
|
||||
case EditAction::eSetTextDecorationPropertyUnderline:
|
||||
case EditAction::eRemoveTextDecorationPropertyUnderline:
|
||||
case EditAction::eSetTextDecorationPropertyLineThrough:
|
||||
case EditAction::eRemoveTextDecorationPropertyLineThrough:
|
||||
case EditAction::eSetVerticalAlignPropertySuper:
|
||||
case EditAction::eRemoveVerticalAlignPropertySuper:
|
||||
case EditAction::eSetVerticalAlignPropertySub:
|
||||
case EditAction::eRemoveVerticalAlignPropertySub:
|
||||
case EditAction::eSetFontFamilyProperty:
|
||||
case EditAction::eRemoveFontFamilyProperty:
|
||||
case EditAction::eSetColorProperty:
|
||||
case EditAction::eRemoveColorProperty:
|
||||
case EditAction::eSetBackgroundColorPropertyInline:
|
||||
case EditAction::eRemoveBackgroundColorPropertyInline:
|
||||
case EditAction::eRemoveAllInlineStyleProperties:
|
||||
case EditAction::eIncrementFontSize:
|
||||
case EditAction::eDecrementFontSize:
|
||||
case EditAction::eSetAlignment:
|
||||
case EditAction::eAlignLeft:
|
||||
case EditAction::eAlignRight:
|
||||
case EditAction::eAlignCenter:
|
||||
case EditAction::eJustify:
|
||||
case EditAction::eSetBackgroundColor:
|
||||
case EditAction::eSetPositionToAbsoluteOrStatic:
|
||||
case EditAction::eIncreaseOrDecreaseZIndex:
|
||||
return false;
|
||||
|
||||
// EditActions controlling editor feature or state.
|
||||
case EditAction::eEnableOrDisableCSS:
|
||||
case EditAction::eEnableOrDisableAbsolutePositionEditor:
|
||||
case EditAction::eEnableOrDisableResizer:
|
||||
case EditAction::eEnableOrDisableInlineTableEditingUI:
|
||||
case EditAction::eSetCharacterSet:
|
||||
case EditAction::eSetWrapWidth:
|
||||
case EditAction::eRewrap:
|
||||
return false;
|
||||
|
||||
case EditAction::eSetText:
|
||||
case EditAction::eSetHTML:
|
||||
return true;
|
||||
|
||||
case EditAction::eInsertHTML:
|
||||
return true;
|
||||
|
||||
case EditAction::eHidePassword:
|
||||
case EditAction::eCreatePaddingBRElementForEmptyEditor:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
inline bool operator!(const mozilla::EditSubAction& aEditSubAction) {
|
||||
|
|
|
@ -3581,6 +3581,29 @@ EditorBase::CreateTransactionForCollapsedRange(
|
|||
return deleteNodeTransaction.forget();
|
||||
}
|
||||
|
||||
bool EditorBase::FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
|
||||
nsIEditor::EDirection aDirectionAndAmount) const {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
if (NS_WARN_IF(Destroyed())) {
|
||||
return false;
|
||||
}
|
||||
if (!EditorUtils::IsFrameSelectionRequiredToExtendSelection(
|
||||
aDirectionAndAmount, *SelectionRefPtr())) {
|
||||
return true;
|
||||
}
|
||||
// Although AutoRangeArray::ExtendAnchorFocusRangeFor() will use
|
||||
// nsFrameSelection, if it still has dirty frame, nsFrameSelection doesn't
|
||||
// extend selection since we block script.
|
||||
if (RefPtr<PresShell> presShell = GetPresShell()) {
|
||||
presShell->FlushPendingNotifications(FlushType::Layout);
|
||||
if (NS_WARN_IF(Destroyed())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
nsresult EditorBase::DeleteSelectionAsAction(
|
||||
nsIEditor::EDirection aDirectionAndAmount,
|
||||
nsIEditor::EStripWrappers aStripWrappers, nsIPrincipal* aPrincipal) {
|
||||
|
@ -3682,47 +3705,15 @@ nsresult EditorBase::DeleteSelectionAsAction(
|
|||
}
|
||||
}
|
||||
|
||||
if (EditorUtils::IsFrameSelectionRequiredToExtendSelection(
|
||||
aDirectionAndAmount, *SelectionRefPtr())) {
|
||||
// Although AutoRangeArray::ExtendAnchorFocusRangeFor() will use
|
||||
// nsFrameSelection, if it still has dirty frame, nsFrameSelection doesn't
|
||||
// extend selection since we block script.
|
||||
if (RefPtr<PresShell> presShell = GetPresShell()) {
|
||||
presShell->FlushPendingNotifications(FlushType::Layout);
|
||||
if (NS_WARN_IF(Destroyed())) {
|
||||
editActionData.Abort();
|
||||
return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
|
||||
}
|
||||
}
|
||||
if (!FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
|
||||
aDirectionAndAmount)) {
|
||||
NS_WARNING("Flusing pending notifications caused destroying the editor");
|
||||
editActionData.Abort();
|
||||
return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_DESTROYED);
|
||||
}
|
||||
|
||||
// TODO: This should be done when selection is not collapsed and the edit
|
||||
// action requires to delete the range first.
|
||||
if (IsHTMLEditor() && editActionData.NeedsToDispatchBeforeInputEvent()) {
|
||||
AutoRangeArray rangesToDelete(*SelectionRefPtr());
|
||||
if (!rangesToDelete.Ranges().IsEmpty()) {
|
||||
nsresult rv =
|
||||
MOZ_KnownLive(AsHTMLEditor())
|
||||
->ComputeTargetRanges(aDirectionAndAmount, rangesToDelete);
|
||||
if (rv == NS_ERROR_EDITOR_DESTROYED) {
|
||||
NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor");
|
||||
return NS_ERROR_EDITOR_DESTROYED;
|
||||
}
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rv),
|
||||
"HTMLEditor::ComputeTargetRanges() failed, but ignored");
|
||||
for (auto& range : rangesToDelete.Ranges()) {
|
||||
RefPtr<StaticRange> staticRange =
|
||||
StaticRange::Create(range, IgnoreErrors());
|
||||
if (NS_WARN_IF(!staticRange)) {
|
||||
continue;
|
||||
}
|
||||
editActionData.AppendTargetRange(*staticRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nsresult rv = editActionData.MaybeDispatchBeforeInputEvent();
|
||||
nsresult rv =
|
||||
editActionData.MaybeDispatchBeforeInputEvent(aDirectionAndAmount);
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING_ASSERTION(rv == NS_ERROR_EDITOR_ACTION_CANCELED,
|
||||
"MaybeDispatchBeforeInputEvent() failed");
|
||||
|
@ -3742,6 +3733,11 @@ nsresult EditorBase::DeleteSelectionAsSubAction(
|
|||
nsIEditor::EDirection aDirectionAndAmount,
|
||||
nsIEditor::EStripWrappers aStripWrappers) {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
// If handling edit action is for table editing, this may be called with
|
||||
// selecting an any table element by the caller, but it's not usual work of
|
||||
// this so that `MayEditActionDeleteSelection()` returns false.
|
||||
MOZ_ASSERT(MayEditActionDeleteSelection(GetEditAction()) ||
|
||||
IsEditActionTableEditing(GetEditAction()));
|
||||
MOZ_ASSERT(mPlaceholderBatch);
|
||||
MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
|
||||
NS_ASSERTION(IsHTMLEditor() || aStripWrappers == nsIEditor::eNoStrip,
|
||||
|
@ -5307,11 +5303,14 @@ bool EditorBase::AutoEditActionDataSetter::IsBeforeInputEventEnabled() const {
|
|||
return true;
|
||||
}
|
||||
|
||||
nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent() {
|
||||
nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent(
|
||||
nsIEditor::EDirection aDeleteDirectionAndAmount /* nsIEditor::eNone */) {
|
||||
MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
|
||||
"We've already handled beforeinput event");
|
||||
MOZ_ASSERT(CanHandle());
|
||||
MOZ_ASSERT(!IsBeforeInputEventEnabled() || NeedsToDispatchBeforeInputEvent());
|
||||
MOZ_ASSERT_IF(!MayEditActionDeleteAroundCollapsedSelection(mEditAction),
|
||||
aDeleteDirectionAndAmount == nsIEditor::eNone);
|
||||
|
||||
mHasTriedToDispatchBeforeInputEvent = true;
|
||||
|
||||
|
@ -5333,28 +5332,63 @@ nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent() {
|
|||
}
|
||||
OwningNonNull<TextEditor> textEditor = *mEditorBase.AsTextEditor();
|
||||
EditorInputType inputType = ToInputType(mEditAction);
|
||||
// If mTargetRanges has not been initialized yet, it means that we may need
|
||||
// to set it to selection ranges.
|
||||
if (textEditor->AsHTMLEditor() && mTargetRanges.IsEmpty() &&
|
||||
MayHaveTargetRangesOnHTMLEditor(inputType)) {
|
||||
if (uint32_t rangeCount = textEditor->SelectionRefPtr()->RangeCount()) {
|
||||
mTargetRanges.SetCapacity(rangeCount);
|
||||
for (uint32_t i = 0; i < rangeCount; i++) {
|
||||
const nsRange* range = textEditor->SelectionRefPtr()->GetRangeAt(i);
|
||||
if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
|
||||
continue;
|
||||
if (textEditor->IsHTMLEditor() && mTargetRanges.IsEmpty()) {
|
||||
// If the edit action will delete selected ranges, compute the range
|
||||
// strictly.
|
||||
if (MayEditActionDeleteAroundCollapsedSelection(mEditAction) ||
|
||||
(!textEditor->SelectionRefPtr()->IsCollapsed() &&
|
||||
MayEditActionDeleteSelection(mEditAction))) {
|
||||
if (!textEditor
|
||||
->FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
|
||||
aDeleteDirectionAndAmount)) {
|
||||
NS_WARNING(
|
||||
"Flusing pending notifications caused destroying the editor");
|
||||
return NS_ERROR_EDITOR_DESTROYED;
|
||||
}
|
||||
|
||||
AutoRangeArray rangesToDelete(*textEditor->SelectionRefPtr());
|
||||
if (!rangesToDelete.Ranges().IsEmpty()) {
|
||||
nsresult rv = MOZ_KnownLive(textEditor->AsHTMLEditor())
|
||||
->ComputeTargetRanges(aDeleteDirectionAndAmount,
|
||||
rangesToDelete);
|
||||
if (rv == NS_ERROR_EDITOR_DESTROYED) {
|
||||
NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor");
|
||||
return NS_ERROR_EDITOR_DESTROYED;
|
||||
}
|
||||
// Now, we need to fix the offset of target range because it may
|
||||
// be referred after modifying the DOM tree and range boundaries
|
||||
// of `range` may have not computed offset yet.
|
||||
RefPtr<StaticRange> targetRange = StaticRange::Create(
|
||||
range->GetStartContainer(), range->StartOffset(),
|
||||
range->GetEndContainer(), range->EndOffset(), IgnoreErrors());
|
||||
if (NS_WARN_IF(!targetRange) ||
|
||||
NS_WARN_IF(!targetRange->IsPositioned())) {
|
||||
continue;
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rv),
|
||||
"HTMLEditor::ComputeTargetRanges() failed, but ignored");
|
||||
for (auto& range : rangesToDelete.Ranges()) {
|
||||
RefPtr<StaticRange> staticRange =
|
||||
StaticRange::Create(range, IgnoreErrors());
|
||||
if (NS_WARN_IF(!staticRange)) {
|
||||
continue;
|
||||
}
|
||||
AppendTargetRange(*staticRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, just set target ranges to selection ranges.
|
||||
else if (MayHaveTargetRangesOnHTMLEditor(inputType)) {
|
||||
if (uint32_t rangeCount = textEditor->SelectionRefPtr()->RangeCount()) {
|
||||
mTargetRanges.SetCapacity(rangeCount);
|
||||
for (uint32_t i = 0; i < rangeCount; i++) {
|
||||
const nsRange* range = textEditor->SelectionRefPtr()->GetRangeAt(i);
|
||||
if (NS_WARN_IF(!range) || NS_WARN_IF(!range->IsPositioned())) {
|
||||
continue;
|
||||
}
|
||||
// Now, we need to fix the offset of target range because it may
|
||||
// be referred after modifying the DOM tree and range boundaries
|
||||
// of `range` may have not computed offset yet.
|
||||
RefPtr<StaticRange> targetRange = StaticRange::Create(
|
||||
range->GetStartContainer(), range->StartOffset(),
|
||||
range->GetEndContainer(), range->EndOffset(), IgnoreErrors());
|
||||
if (NS_WARN_IF(!targetRange) ||
|
||||
NS_WARN_IF(!targetRange->IsPositioned())) {
|
||||
continue;
|
||||
}
|
||||
mTargetRanges.AppendElement(std::move(targetRange));
|
||||
}
|
||||
mTargetRanges.AppendElement(std::move(targetRange));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -861,11 +861,16 @@ class EditorBase : public nsIEditor,
|
|||
* dispatch "beforeinput" event or not. Then,
|
||||
* mHasTriedToDispatchBeforeInputEvent is set to true.
|
||||
*
|
||||
* @param aDeleteDirectionAndAmount
|
||||
* If `MayEditActionDeleteAroundCollapsedSelection(
|
||||
* mEditAction)` returns true, this must be set.
|
||||
* Otherwise, don't set explicitly.
|
||||
* @return If this method actually dispatches "beforeinput" event
|
||||
* and it's canceled, returns
|
||||
* NS_ERROR_EDITOR_ACTION_CANCELED.
|
||||
*/
|
||||
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent();
|
||||
[[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult MaybeDispatchBeforeInputEvent(
|
||||
nsIEditor::EDirection aDeleteDirectionAndAmount = nsIEditor::eNone);
|
||||
|
||||
/**
|
||||
* MarkAsBeforeInputHasBeenDispatched() should be called only when updating
|
||||
|
@ -1879,6 +1884,16 @@ class EditorBase : public nsIEditor,
|
|||
*/
|
||||
void UndefineCaretBidiLevel() const;
|
||||
|
||||
/**
|
||||
* Flushing pending notifications if nsFrameSelection requires the latest
|
||||
* layout information to compute deletion range. This may destroy the
|
||||
* editor instance itself. When this returns false, don't keep doing
|
||||
* anything.
|
||||
*/
|
||||
[[nodiscard]] MOZ_CAN_RUN_SCRIPT bool
|
||||
FlushPendingNotificationsIfToHandleDeletionWithFrameSelection(
|
||||
nsIEditor::EDirection aDirectionAndAmount) const;
|
||||
|
||||
/**
|
||||
* DeleteSelectionAsSubAction() removes selection content or content around
|
||||
* caret with transactions. This should be used for handling it as an
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[input-events-get-target-ranges-backspace.tentative.html]
|
||||
max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly.
|
||||
min-asserts: 0 # An assertion in the constructor of TextFragmentData hits twice or not.
|
||||
max-asserts: 2 # An assertion in the constructor of TextFragmentData
|
||||
min-asserts: 0 # But sometimes not counted correctly
|
||||
prefs: [editor.hr_element.allow_to_delete_from_following_line:true]
|
||||
[Alt + Backspace at "<p>abc def[\] ghi</p>"]
|
||||
expected:
|
||||
|
@ -40,12 +40,6 @@
|
|||
[Backspace at "<div>abc <ul><li>[\] def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Backspace at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Backspace at "<p>abc[</p><p>}<br></p>"]
|
||||
expected: FAIL
|
||||
|
||||
[Backspace at "<p>abc<span contenteditable=\"false\">def</span>[\]ghi</p>"]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[input-events-get-target-ranges-forwarddelete.tentative.html]
|
||||
max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly.
|
||||
min-asserts: 0 # An assertion in the constructor of TextFragmentData hits twice or not.
|
||||
max-asserts: 5 # An assertion in the constructor of TextFragmentData
|
||||
min-asserts: 0 # But sometimes not counted correctly
|
||||
[Alt + Delete at "<p>abc [\]def ghi</p>"]
|
||||
expected:
|
||||
if (os == "android"): FAIL
|
||||
|
@ -69,14 +69,8 @@
|
|||
[Delete at "<div>abc [\]<ul><li> def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Delete at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Delete at "<div>abc <ul><li> def[\] </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Delete at "<p>{<br></p><p>\]abc</p>"]
|
||||
expected: FAIL
|
||||
|
||||
[Delete at "<p>abc[\]<span contenteditable=\"false\">def</span>ghi</p>"]
|
||||
expected: FAIL
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
[input-events-get-target-ranges-non-collapsed-selection.tentative.html?Backspace]
|
||||
max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly.
|
||||
min-asserts: 0
|
||||
prefs: [editor.hr_element.allow_to_delete_from_following_line:true]
|
||||
[Backspace at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Backspace at "<p>abc[</p><p>}<br></p>"]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[input-events-get-target-ranges-non-collapsed-selection.tentative.html?Delete]
|
||||
max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly.
|
||||
min-asserts: 0
|
||||
[Delete at "<p>abc[</p><p>}<br></p>"]
|
||||
expected: FAIL
|
||||
|
||||
[Delete at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[Delete at "<p>{<br></p><p>\]abc</p>"]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[input-events-get-target-ranges-non-collapsed-selection.tentative.html?TypingA]
|
||||
max-asserts: 11 # The assertion in nsTableCellFrame::DecorateForSelection() hits randomly.
|
||||
min-asserts: 0
|
||||
[TypingA at "<div>abc [<ul><li>\] def </li></ul> ghi</div>"]
|
||||
expected: FAIL
|
||||
|
||||
[TypingA at "<p>abc[</p><p>}<br></p>"]
|
||||
expected: FAIL
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,566 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<meta name="timeout" content="long">
|
||||
<meta name="variant" content="?Backspace">
|
||||
<meta name="variant" content="?Delete">
|
||||
<meta name="variant" content="?TypingA">
|
||||
<title>InputEvent.getTargetRanges() with non-collapsed selection</title>
|
||||
<div contenteditable></div>
|
||||
<script src="input-events-get-target-ranges.js"></script>
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="/resources/testdriver.js"></script>
|
||||
<script src="/resources/testdriver-vendor.js"></script>
|
||||
<script src="/resources/testdriver-actions.js"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
let action = location.search.substring(1);
|
||||
function run() {
|
||||
switch (action) {
|
||||
case "Backspace":
|
||||
return sendBackspaceKey();
|
||||
case "Delete":
|
||||
return sendDeleteKey();
|
||||
case "TypingA":
|
||||
return sendKeyA();
|
||||
default:
|
||||
throw "Unhandled variant";
|
||||
}
|
||||
}
|
||||
|
||||
let insertedHTML = action === "TypingA" ? "a" : "";
|
||||
|
||||
// Invisible leading white-spaces in current block and invisible trailing
|
||||
// white-spaces in the previous block should be deleted for avoiding they
|
||||
// becoming visible when the blocks are joined. Perhaps, they should be
|
||||
// contained by the range of `getTargetRanges()`, but needs discussion.
|
||||
// https://github.com/w3c/input-events/issues/112
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc </p><p> def</p>");
|
||||
let p1 = gEditor.firstChild;
|
||||
let abc = p1.firstChild;
|
||||
let p2 = p1.nextSibling;
|
||||
let def = p2.firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 6, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 3,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>abc [</p><p>] def</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc</p><p>def</p>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p + p").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 2, def, 1);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>ab${insertedHTML}ef</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 2,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>ab[c</p><p>d]ef</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc </p><p> def</p>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p + p").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 2, def, 2);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>ab${insertedHTML}ef</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 2,
|
||||
endContainer: def,
|
||||
endOffset: 2,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>ab[c </p><p> d]ef</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc </p><p> def</p>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p + p").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 2, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>ab${insertedHTML}def</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 2,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>ab[c </p><p>] def</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc </p><p> def</p>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p + p").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 4, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>abc [</p><p>] def</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc </p><p> def</p>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p + p").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 4, def, 1);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>abc [</p><p> ]def</p>"`);
|
||||
|
||||
// Different from collapsed range around an atomic content, non-collapsed
|
||||
// range may not be shrunken to select only the atomic content for avoid
|
||||
// to waste runtime cost.
|
||||
promise_test(async () => {
|
||||
initializeTest(`<p>abc<img src="${kImgSrc}">def</p>`);
|
||||
let p = gEditor.querySelector("p");
|
||||
let abc = p.firstChild;
|
||||
let def = p.lastChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>abc[<img>]def</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest(`<div>abc<hr>def</div>`);
|
||||
let div = gEditor.querySelector("div");
|
||||
let abc = div.firstChild;
|
||||
let def = div.lastChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc[<hr>]def</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest(`<div>abc <hr>def</div>`);
|
||||
let div = gEditor.querySelector("div");
|
||||
let abc = div.firstChild;
|
||||
let def = div.lastChild;
|
||||
gSelection.setBaseAndExtent(abc, 4, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc [<hr>]def</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest(`<div>abc <hr> def</div>`);
|
||||
let div = gEditor.querySelector("div");
|
||||
let abc = div.firstChild;
|
||||
let def = div.lastChild;
|
||||
gSelection.setBaseAndExtent(abc, 4, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc [<hr>] def</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest(`<div>abc <hr> def</div>`);
|
||||
let div = gEditor.querySelector("div");
|
||||
let abc = div.firstChild;
|
||||
let def = div.lastChild;
|
||||
gSelection.setBaseAndExtent(div, 1, div, 2);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc {<hr>} def</div>"`);
|
||||
|
||||
// Deleting visible `<br>` element should be contained by a range of
|
||||
// `getTargetRanges()`. However, when only the `<br>` element is selected,
|
||||
// the range shouldn't start from nor end by surrounding text nodes?
|
||||
// https://github.com/w3c/input-events/issues/112
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc<br>def</p>");
|
||||
gSelection.setBaseAndExtent(gEditor.firstChild, 1, gEditor.firstChild, 2);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<p>abc${insertedHTML}def</p>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: gEditor.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild,
|
||||
endOffset: 2,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>abc{<br>}def</p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<p>def<br>ghi</p></div>");
|
||||
let p = gEditor.querySelector("p");
|
||||
let def = p.firstChild;
|
||||
let abc = gEditor.firstChild.firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, def, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<div>abc${insertedHTML}def<p>ghi</p></div>`,
|
||||
`<div>abc${insertedHTML}def<br><p>ghi</p></div>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc[<p>]def<br>ghi</p></div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc <p> def<br>ghi</p></div>");
|
||||
let p = gEditor.querySelector("p");
|
||||
let def = p.firstChild;
|
||||
let abc = gEditor.firstChild.firstChild;
|
||||
gSelection.setBaseAndExtent(abc, abc.length, def, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<div>abc${insertedHTML}def<p>ghi</p></div>`,
|
||||
`<div>abc${insertedHTML}def<br><p>ghi</p></div>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 3,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc [<p>] def<br>ghi</p></div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div><p>abc</p>def</div>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p").nextSibling;
|
||||
gSelection.setBaseAndExtent(abc, 3, def, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<div><p>abc${insertedHTML}def</p></div>`,
|
||||
`<div><p>abc${insertedHTML}def<br></p></div>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div><p>abc[</p>]def</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div><p>abc </p> def</div>");
|
||||
let abc = gEditor.querySelector("p").firstChild;
|
||||
let def = gEditor.querySelector("p").nextSibling;
|
||||
gSelection.setBaseAndExtent(abc, abc.length, def, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<div><p>abc${insertedHTML}def</p></div>`,
|
||||
`<div><p>abc${insertedHTML}def<br></p></div>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 3,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div><p>abc [</p>] def</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
|
||||
let abc = gEditor.querySelector("div").firstChild;
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}defghi</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc[<ul><li>]def</li></ul>ghi</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
|
||||
let abc = gEditor.querySelector("div").firstChild;
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, abc.length, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}defghi</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc [<ul><li>] def </li></ul> ghi</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li></ul>ghi</div>");
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
let ghi = gEditor.querySelector("ul").nextSibling;
|
||||
gSelection.setBaseAndExtent(def, 3, ghi, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def${insertedHTML}ghi</li></ul></div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: def,
|
||||
startOffset: 3,
|
||||
endContainer: ghi,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc<ul><li>def[</li></ul>]ghi</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc <ul><li> def </li></ul> ghi</div>");
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
let ghi = gEditor.querySelector("ul").nextSibling;
|
||||
gSelection.setBaseAndExtent(def, def.length, ghi, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<div>abc <ul><li> def${insertedHTML}ghi</li></ul></div>`,
|
||||
`<div>abc <ul><li>def${insertedHTML}ghi</li></ul></div>`,
|
||||
`<div>abc<ul><li>def${insertedHTML}ghi</li></ul></div>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: def,
|
||||
startOffset: 5,
|
||||
endContainer: ghi,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc <ul><li> def [</li></ul>] ghi</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
|
||||
let abc = gEditor.querySelector("div").firstChild;
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, def, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc${insertedHTML}def<ul><li>ghi</li></ul>jkl</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: def,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc[<ul><li>]def</li><li>ghi</li></ul>jkl</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
|
||||
let abc = gEditor.querySelector("div").firstChild;
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
let ghi = gEditor.querySelector("li + li").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, ghi, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<div>abc${insertedHTML}ghijkl</div>`,
|
||||
`<div>abc${insertedHTML}ghijkl<br></div>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: ghi,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc[<ul><li>def</li><li>]ghi</li></ul>jkl</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
let ghi = gEditor.querySelector("li + li").firstChild;
|
||||
gSelection.setBaseAndExtent(def, 3, ghi, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def${insertedHTML}ghi</li></ul>jkl</div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: def,
|
||||
startOffset: 3,
|
||||
endContainer: ghi,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc<ul><li>def[</li><li>]ghi</li></ul>jkl</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
|
||||
let ghi = gEditor.querySelector("li + li").firstChild;
|
||||
let jkl = gEditor.querySelector("ul").nextSibling;
|
||||
gSelection.setBaseAndExtent(ghi, 3, jkl, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def</li><li>ghi${insertedHTML}jkl</li></ul></div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: ghi,
|
||||
startOffset: 3,
|
||||
endContainer: jkl,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc<ul><li>def</li><li>ghi[</li></ul>]jkl</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<div>abc<ul><li>def</li><li>ghi</li></ul>jkl</div>");
|
||||
let def = gEditor.querySelector("li").firstChild;
|
||||
let jkl = gEditor.querySelector("ul").nextSibling;
|
||||
gSelection.setBaseAndExtent(def, 3, jkl, 0);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<div>abc<ul><li>def${insertedHTML}jkl</li></ul></div>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: def,
|
||||
startOffset: 3,
|
||||
endContainer: jkl,
|
||||
endOffset: 0,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<div>abc<ul><li>def[</li><li>ghi</li></ul>]jkl</div>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc</p><p><br></p>");
|
||||
let p1 = gEditor.querySelector("p");
|
||||
let abc = p1.firstChild;
|
||||
let p2 = p1.nextSibling;
|
||||
gSelection.setBaseAndExtent(abc, 3, p2, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<p>abc${insertedHTML}</p>`,
|
||||
`<p>abc${insertedHTML}<br></p>`]);
|
||||
if (gEditor.innerHTML === "<p>abc</p>") {
|
||||
// Include the invisible `<br>` element if it's deleted.
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: p2,
|
||||
endOffset: 1,
|
||||
});
|
||||
} else {
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: p2,
|
||||
endOffset: 0,
|
||||
});
|
||||
}
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<p>abc[</p><p>}<br></p>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<p>abc<span contenteditable=\"false\">def</span>ghi</p>");
|
||||
let p = gEditor.querySelector("p");
|
||||
let abc = p.firstChild;
|
||||
let ghi = p.lastChild;
|
||||
gSelection.setBaseAndExtent(abc, 3, ghi, 0);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, ["<p>abc<span contenteditable=\"false\">def</span>ghi</p>",
|
||||
`<p>abc${insertedHTML}ghi</p>`,
|
||||
`<p>abc${insertedHTML}ghi<br></p>`]);
|
||||
// Don't need to shrink the range for avoiding to waste runtime cost.
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 3,
|
||||
endContainer: ghi,
|
||||
endOffset: 0,
|
||||
});
|
||||
if (gEditor.innerHTML === "<p>abc<span contenteditable=\"false\">def</span>ghi</p>") {
|
||||
checkGetTargetRangesOfInputOnDoNothing();
|
||||
} else {
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}
|
||||
}, `${action} at "<p>abc[<span contenteditable=\"false\">def</span>]ghi</p>"`);
|
||||
|
||||
// The table structure shouldn't be modified when deleting cell contents,
|
||||
// in this case, getTargetRanges() should return multiple ranges in each
|
||||
// cell?
|
||||
promise_test(async () => {
|
||||
initializeTest("<table><tr><td>abc</td><td>def</td></tr></table>");
|
||||
let abc = gEditor.querySelector("td").firstChild;
|
||||
let def = gEditor.querySelector("td + td").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 2, def, 1);
|
||||
await run();
|
||||
assert_equals(gEditor.innerHTML, `<table><tbody><tr><td>ab${insertedHTML}</td><td>ef</td></tr></tbody></table>`);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 2,
|
||||
endContainer: def,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<table><tr><td>ab[c</td><td>d]ef</td></tr></table>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<table><tr><td>abc</td><td>def</td></tr><tr><td>ghi</td><td>jkl</td></tr></table>");
|
||||
let abc = gEditor.querySelector("td").firstChild;
|
||||
let jkl = gEditor.querySelector("tr + tr > td + td").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 2, jkl, 1);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<table><tbody><tr><td>ab${insertedHTML}</td><td></td></tr><tr><td></td><td>kl</td></tr></tbody></table>`,
|
||||
`<table><tbody><tr><td>ab${insertedHTML}</td><td><br></td></tr><tr><td><br></td><td>kl</td></tr></tbody></table>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 2,
|
||||
endContainer: jkl,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<table><tr><td>ab[c</td><td>def</td></tr><tr><td>ghi</td><td>j]kl</td></tr></table>"`);
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest("<table><tr><td>abc</td><td>def</td></tr></table><table><tr><td>ghi</td><td>jkl</td></tr></table>");
|
||||
let abc = gEditor.querySelector("td").firstChild;
|
||||
let jkl = gEditor.querySelector("table + table td + td").firstChild;
|
||||
gSelection.setBaseAndExtent(abc, 2, jkl, 1);
|
||||
await run();
|
||||
assert_in_array(gEditor.innerHTML, [`<table><tbody><tr><td>ab${insertedHTML}</td><td></td></tr></tbody></table><table><tbody><tr><td></td><td>kl</td></tr></tbody></table>`,
|
||||
`<table><tbody><tr><td>ab${insertedHTML}</td><td><br></td></tr></tbody></table><table><tbody><tr><td><br></td><td>kl</td></tr></tbody></table>`]);
|
||||
checkGetTargetRangesOfBeforeinputOnDeleteSomething({
|
||||
startContainer: abc,
|
||||
startOffset: 2,
|
||||
endContainer: jkl,
|
||||
endOffset: 1,
|
||||
});
|
||||
checkGetTargetRangesOfInputOnDeleteSomething();
|
||||
}, `${action} at "<table><tr><td>ab[c</td><td>def</td></tr></table><table><tr><td>ghi</td><td>j]kl</td></tr></table>"`);
|
||||
|
||||
</script>
|
|
@ -0,0 +1,223 @@
|
|||
"use strict";
|
||||
|
||||
const kBackspaceKey = "\uE003";
|
||||
const kDeleteKey = "\uE017";
|
||||
const kArrowRight = "\uE014";
|
||||
const kArrowLeft = "\uE012";
|
||||
const kShift = "\uE008";
|
||||
const kMeta = "\uE03d";
|
||||
const kControl = "\uE009";
|
||||
const kAlt = "\uE00A";
|
||||
const kKeyA = "a";
|
||||
|
||||
const kImgSrc =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg==";
|
||||
|
||||
let gSelection = getSelection();
|
||||
let gEditor = document.querySelector("div[contenteditable]");
|
||||
let gBeforeinput = [];
|
||||
let gInput = [];
|
||||
gEditor.addEventListener("beforeinput", e => {
|
||||
// NOTE: Blink makes `getTargetRanges()` return empty range after propagation,
|
||||
// but this test wants to check the result during propagation.
|
||||
// Therefore, we need to cache the result, but will assert if
|
||||
// `getTargetRanges()` returns different ranges after checking the
|
||||
// cached ranges.
|
||||
e.cachedRanges = e.getTargetRanges();
|
||||
gBeforeinput.push(e);
|
||||
});
|
||||
gEditor.addEventListener("input", e => {
|
||||
e.cachedRanges = e.getTargetRanges();
|
||||
gInput.push(e);
|
||||
});
|
||||
|
||||
function initializeTest(aInnerHTML) {
|
||||
gEditor.innerHTML = aInnerHTML;
|
||||
gEditor.focus();
|
||||
gBeforeinput = [];
|
||||
gInput = [];
|
||||
}
|
||||
|
||||
function getRangeDescription(range) {
|
||||
function getNodeDescription(node) {
|
||||
if (!node) {
|
||||
return "null";
|
||||
}
|
||||
switch (node.nodeType) {
|
||||
case Node.TEXT_NODE:
|
||||
case Node.COMMENT_NODE:
|
||||
case Node.CDATA_SECTION_NODE:
|
||||
return `${node.nodeName} "${node.data}"`;
|
||||
case Node.ELEMENT_NODE:
|
||||
return `<${node.nodeName.toLowerCase()}>`;
|
||||
default:
|
||||
return `${node.nodeName}`;
|
||||
}
|
||||
}
|
||||
if (range === null) {
|
||||
return "null";
|
||||
}
|
||||
if (range === undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
return range.startContainer == range.endContainer &&
|
||||
range.startOffset == range.endOffset
|
||||
? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
|
||||
: `(${getNodeDescription(range.startContainer)}, ${
|
||||
range.startOffset
|
||||
}) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
|
||||
}
|
||||
|
||||
function getArrayOfRangesDescription(arrayOfRanges) {
|
||||
if (arrayOfRanges === null) {
|
||||
return "null";
|
||||
}
|
||||
if (arrayOfRanges === undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
if (!Array.isArray(arrayOfRanges)) {
|
||||
return "Unknown Object";
|
||||
}
|
||||
if (arrayOfRanges.length === 0) {
|
||||
return "[]";
|
||||
}
|
||||
let result = "[";
|
||||
for (let range of arrayOfRanges) {
|
||||
result += `{${getRangeDescription(range)}},`;
|
||||
}
|
||||
result += "]";
|
||||
return result;
|
||||
}
|
||||
|
||||
function sendDeleteKey(modifier) {
|
||||
if (!modifier) {
|
||||
return new test_driver.Actions()
|
||||
.keyDown(kDeleteKey)
|
||||
.keyUp(kDeleteKey)
|
||||
.send();
|
||||
}
|
||||
return new test_driver.Actions()
|
||||
.keyDown(modifier)
|
||||
.keyDown(kDeleteKey)
|
||||
.keyUp(kDeleteKey)
|
||||
.keyUp(modifier)
|
||||
.send();
|
||||
}
|
||||
|
||||
function sendBackspaceKey(modifier) {
|
||||
if (!modifier) {
|
||||
return new test_driver.Actions()
|
||||
.keyDown(kBackspaceKey)
|
||||
.keyUp(kBackspaceKey)
|
||||
.send();
|
||||
}
|
||||
return new test_driver.Actions()
|
||||
.keyDown(modifier)
|
||||
.keyDown(kBackspaceKey)
|
||||
.keyUp(kBackspaceKey)
|
||||
.keyUp(modifier)
|
||||
.send();
|
||||
}
|
||||
|
||||
function sendKeyA() {
|
||||
return new test_driver.Actions()
|
||||
.keyDown(kKeyA)
|
||||
.keyUp(kKeyA)
|
||||
.send();
|
||||
}
|
||||
|
||||
function sendArrowLeftKey() {
|
||||
return new test_driver.Actions()
|
||||
.keyDown(kArrowLeft)
|
||||
.keyUp(kArrowLeft)
|
||||
.send();
|
||||
}
|
||||
|
||||
function sendArrowRightKey() {
|
||||
return new test_driver.Actions()
|
||||
.keyDown(kArrowRight)
|
||||
.keyUp(kArrowRight)
|
||||
.send();
|
||||
}
|
||||
|
||||
function checkGetTargetRangesKeepReturningSameValue(event) {
|
||||
// https://github.com/w3c/input-events/issues/114
|
||||
assert_equals(
|
||||
getArrayOfRangesDescription(event.getTargetRanges()),
|
||||
getArrayOfRangesDescription(event.cachedRanges),
|
||||
`${event.type}.getTargetRanges() should keep returning the same array of ranges even after its propagation finished`
|
||||
);
|
||||
}
|
||||
|
||||
function checkGetTargetRangesOfBeforeinputOnDeleteSomething(expectedRange) {
|
||||
assert_equals(
|
||||
gBeforeinput.length,
|
||||
1,
|
||||
"One beforeinput event should be fired if the key operation deletes something"
|
||||
);
|
||||
assert_true(
|
||||
Array.isArray(gBeforeinput[0].cachedRanges),
|
||||
"gBeforeinput[0].getTargetRanges() should return an array of StaticRange instances during propagation"
|
||||
);
|
||||
// Before checking the length of array of ranges, we should check the first
|
||||
// range first because the first range data is more important than whether
|
||||
// there are additional unexpected ranges.
|
||||
if (gBeforeinput[0].cachedRanges.length > 0) {
|
||||
assert_equals(
|
||||
getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription(expectedRange),
|
||||
`gBeforeinput[0].getTargetRanges() should return expected range (inputType is "${gBeforeinput[0].inputType}")`
|
||||
);
|
||||
assert_equals(
|
||||
gBeforeinput[0].cachedRanges.length,
|
||||
1,
|
||||
"gBeforeinput[0].getTargetRanges() should return one range within an array"
|
||||
);
|
||||
}
|
||||
assert_equals(
|
||||
gBeforeinput[0].cachedRanges.length,
|
||||
1,
|
||||
"One range should be returned from getTargetRanges() when the key operation deletes something"
|
||||
);
|
||||
checkGetTargetRangesKeepReturningSameValue(gBeforeinput[0]);
|
||||
}
|
||||
|
||||
function checkGetTargetRangesOfInputOnDeleteSomething() {
|
||||
assert_equals(
|
||||
gInput.length,
|
||||
1,
|
||||
"One input event should be fired if the key operation deletes something"
|
||||
);
|
||||
// https://github.com/w3c/input-events/issues/113
|
||||
assert_true(
|
||||
Array.isArray(gInput[0].cachedRanges),
|
||||
"gInput[0].getTargetRanges() should return an array of StaticRange instances during propagation"
|
||||
);
|
||||
assert_equals(
|
||||
gInput[0].cachedRanges.length,
|
||||
0,
|
||||
"gInput[0].getTargetRanges() should return empty array during propagation"
|
||||
);
|
||||
checkGetTargetRangesKeepReturningSameValue(gInput[0]);
|
||||
}
|
||||
|
||||
function checkGetTargetRangesOfInputOnDoNothing() {
|
||||
assert_equals(
|
||||
gInput.length,
|
||||
0,
|
||||
"input event shouldn't be fired when the key operation does not cause modifying the DOM tree"
|
||||
);
|
||||
}
|
||||
|
||||
function checkBeforeinputAndInputEventsOnNOOP() {
|
||||
assert_equals(
|
||||
gBeforeinput.length,
|
||||
0,
|
||||
"beforeinput event shouldn't be fired when the key operation does not cause modifying the DOM tree"
|
||||
);
|
||||
assert_equals(
|
||||
gInput.length,
|
||||
0,
|
||||
"input event shouldn't be fired when the key operation does not cause modifying the DOM tree"
|
||||
);
|
||||
}
|
Загрузка…
Ссылка в новой задаче