зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1677566 - part 3: Ignore non-deletable ranges in `HTMLEditor::HandleDeleteSelection()` r=m_kato
For making delete handlers simpler, and set better target ranges to the corresponding `beforeinput` event, we should ignore non-editable ranges before handling deletion. This patch makes editor stop handling deleteion when a range crosses editing host boundaries. In this case, Gecko has done nothing, but fired `beforeinput` event. Note that Blink deletes editable contents in the range **until** it meets first non-editable content, but I don't think this is a good behavior because it makes things complicated. Therefore, I filed a spec issue: https://github.com/w3c/editing/issues/283 On the other hand, this behavior change causes different behavior in https://searchfox.org/mozilla-central/source/editor/libeditor/crashtests/1345015.html It tries to insert paragraph into `<html>` element, but our editor currently does not support it. Therefore, it hits `MOZ_ASSERT`. Therefore, this patch added a new check into `HTMLEditor::InsertParagraphSeparatorAsSubAction()`. Differential Revision: https://phabricator.services.mozilla.com/D107588
This commit is contained in:
Родитель
1229430221
Коммит
7af10cee55
|
@ -5385,6 +5385,11 @@ nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent(
|
|||
NS_WARNING("HTMLEditor::ComputeTargetRanges() destroyed the editor");
|
||||
return NS_ERROR_EDITOR_DESTROYED;
|
||||
}
|
||||
if (rv == NS_ERROR_EDITOR_NO_EDITABLE_RANGE) {
|
||||
// For now, keep dispatching `beforeinput` event even if no selection
|
||||
// range can be editable.
|
||||
rv = NS_OK;
|
||||
}
|
||||
NS_WARNING_ASSERTION(
|
||||
NS_SUCCEEDED(rv),
|
||||
"HTMLEditor::ComputeTargetRanges() failed, but ignored");
|
||||
|
|
|
@ -2084,6 +2084,13 @@ class EditorBase : public nsIEditor,
|
|||
// DOM_SUCCESS_DOM_NO_OPERATION here.
|
||||
case NS_ERROR_EDITOR_ACTION_CANCELED:
|
||||
return NS_SUCCESS_DOM_NO_OPERATION;
|
||||
// If there is no selection range or editable selection ranges, editor
|
||||
// needs to stop handling it. However, editor shouldn't return error for
|
||||
// the callers to avoid throwing exception. However, they may want to
|
||||
// check whether it works or not. Therefore, we should return
|
||||
// NS_SUCCESS_DOM_NO_OPERATION instead.
|
||||
case NS_ERROR_EDITOR_NO_EDITABLE_RANGE:
|
||||
return NS_SUCCESS_DOM_NO_OPERATION;
|
||||
default:
|
||||
return aRv;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,56 @@ EditActionResult& EditActionResult::operator|=(
|
|||
* mozilla::AutoRangeArray
|
||||
*****************************************************************************/
|
||||
|
||||
// static
|
||||
bool AutoRangeArray::IsEditableRange(const dom::AbstractRange& aRange,
|
||||
const Element& aEditingHost) {
|
||||
// TODO: Perhaps, we should check whether the start/end boundaries are
|
||||
// first/last point of non-editable element.
|
||||
// See https://github.com/w3c/editing/issues/283#issuecomment-788654850
|
||||
EditorRawDOMPoint atStart(aRange.StartRef());
|
||||
const bool isStartEditable =
|
||||
atStart.IsInContentNode() &&
|
||||
EditorUtils::IsEditableContent(*atStart.ContainerAsContent(),
|
||||
EditorUtils::EditorType::HTML);
|
||||
if (!isStartEditable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!aRange.Collapsed()) {
|
||||
EditorRawDOMPoint atEnd(aRange.EndRef());
|
||||
const bool isEndEditable =
|
||||
atEnd.IsInContentNode() &&
|
||||
EditorUtils::IsEditableContent(*atEnd.ContainerAsContent(),
|
||||
EditorUtils::EditorType::HTML);
|
||||
if (!isEndEditable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now, both start and end points are editable, but if they are in
|
||||
// different editing host, we cannot edit the range.
|
||||
if (atStart.ContainerAsContent() != atEnd.ContainerAsContent() &&
|
||||
atStart.ContainerAsContent()->GetEditingHost() !=
|
||||
atEnd.ContainerAsContent()->GetEditingHost()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLEditor does not support modifying outside `<body>` element for now.
|
||||
nsINode* commonAncestor = aRange.GetClosestCommonInclusiveAncestor();
|
||||
return commonAncestor && commonAncestor->IsContent() &&
|
||||
commonAncestor->IsInclusiveDescendantOf(&aEditingHost);
|
||||
}
|
||||
|
||||
void AutoRangeArray::EnsureOnlyEditableRanges(const Element& aEditingHost) {
|
||||
for (size_t i = mRanges.Length(); i > 0; i--) {
|
||||
const OwningNonNull<nsRange>& range = mRanges[i - 1];
|
||||
if (!AutoRangeArray::IsEditableRange(range, aEditingHost)) {
|
||||
mRanges.RemoveElementAt(i - 1);
|
||||
}
|
||||
}
|
||||
mAnchorFocusRange = mRanges.IsEmpty() ? nullptr : mRanges.LastElement().get();
|
||||
}
|
||||
|
||||
Result<nsIEditor::EDirection, nsresult>
|
||||
AutoRangeArray::ExtendAnchorFocusRangeFor(
|
||||
const EditorBase& aEditorBase, nsIEditor::EDirection aDirectionAndAmount) {
|
||||
|
@ -116,6 +166,14 @@ AutoRangeArray::ExtendAnchorFocusRangeFor(
|
|||
return Err(NS_ERROR_NOT_INITIALIZED);
|
||||
}
|
||||
|
||||
RefPtr<Element> editingHost;
|
||||
if (aEditorBase.IsHTMLEditor()) {
|
||||
editingHost = aEditorBase.AsHTMLEditor()->GetActiveEditingHost();
|
||||
if (!editingHost) {
|
||||
return Err(NS_ERROR_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
Result<RefPtr<nsRange>, nsresult> result(NS_ERROR_UNEXPECTED);
|
||||
nsIEditor::EDirection directionAndAmountResult = aDirectionAndAmount;
|
||||
switch (aDirectionAndAmount) {
|
||||
|
@ -243,6 +301,12 @@ AutoRangeArray::ExtendAnchorFocusRangeFor(
|
|||
return directionAndAmountResult;
|
||||
}
|
||||
|
||||
// If the new range isn't editable, keep using the original range.
|
||||
if (aEditorBase.IsHTMLEditor() &&
|
||||
!AutoRangeArray::IsEditableRange(*extendedRange, *editingHost)) {
|
||||
return aDirectionAndAmount;
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
|
||||
extendedRange->GetStartContainer())) ||
|
||||
NS_WARN_IF(!frameSelection->IsValidSelectionPoint(
|
||||
|
|
|
@ -756,6 +756,15 @@ class MOZ_STACK_CLASS AutoRangeArray final {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EnsureOnlyEditableRanges() removes ranges which cannot modify.
|
||||
* Note that this is designed only for `HTMLEditor` because this must not
|
||||
* be required by `TextEditor`.
|
||||
*/
|
||||
void EnsureOnlyEditableRanges(const dom::Element& aEditingHost);
|
||||
static bool IsEditableRange(const dom::AbstractRange& aRange,
|
||||
const dom::Element& aEditingHost);
|
||||
|
||||
auto& Ranges() { return mRanges; }
|
||||
const auto& Ranges() const { return mRanges; }
|
||||
auto& FirstRangeRef() { return mRanges[0]; }
|
||||
|
@ -928,6 +937,12 @@ class MOZ_STACK_CLASS AutoRangeArray final {
|
|||
return FocusRef().IsSet() ? FocusRef().GetChildAtOffset() : nullptr;
|
||||
}
|
||||
|
||||
void RemoveAllRanges() {
|
||||
mRanges.Clear();
|
||||
mAnchorFocusRange = nullptr;
|
||||
mDirection = nsDirection::eDirNext;
|
||||
}
|
||||
|
||||
private:
|
||||
AutoTArray<mozilla::OwningNonNull<nsRange>, 8> mRanges;
|
||||
RefPtr<nsRange> mAnchorFocusRange;
|
||||
|
|
|
@ -1247,6 +1247,19 @@ EditActionResult HTMLEditor::InsertParagraphSeparatorAsSubAction() {
|
|||
if (NS_WARN_IF(!editingHost)) {
|
||||
return EditActionIgnored(NS_ERROR_FAILURE);
|
||||
}
|
||||
// If the editing host parent element is editable, it means that the editing
|
||||
// host must be a <body> element and the selection may be outside the body
|
||||
// element. If the selection is outside the editing host, we should not
|
||||
// insert new paragraph nor <br> element.
|
||||
// XXX Currently, we don't support editing outside <body> element, but Blink
|
||||
// does it.
|
||||
if (editingHost->GetParentElement() &&
|
||||
HTMLEditUtils::IsSimplyEditableNode(*editingHost->GetParentElement()) &&
|
||||
(!atStartOfSelection.IsInContentNode() ||
|
||||
!nsContentUtils::ContentIsFlattenedTreeDescendantOf(
|
||||
atStartOfSelection.ContainerAsContent(), editingHost))) {
|
||||
return EditActionHandled(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
|
||||
}
|
||||
|
||||
// Look for the nearest parent block. However, don't return error even if
|
||||
// there is no block parent here because in such case, i.e., editing host
|
||||
|
|
|
@ -6118,7 +6118,9 @@ bool HTMLEditor::IsActiveInDOMWindow() const {
|
|||
return true;
|
||||
}
|
||||
|
||||
Element* HTMLEditor::GetActiveEditingHost() const {
|
||||
Element* HTMLEditor::GetActiveEditingHost(
|
||||
LimitInBodyElement aLimitInBodyElement /* = LimitInBodyElement::Yes */)
|
||||
const {
|
||||
Document* document = GetDocument();
|
||||
if (NS_WARN_IF(!document)) {
|
||||
return nullptr;
|
||||
|
@ -6145,7 +6147,17 @@ Element* HTMLEditor::GetActiveEditingHost() const {
|
|||
content->HasIndependentSelection()) {
|
||||
return nullptr;
|
||||
}
|
||||
return content->GetEditingHost();
|
||||
Element* candidateEditingHost = content->GetEditingHost();
|
||||
if (!candidateEditingHost) {
|
||||
return nullptr;
|
||||
}
|
||||
// Currently, we don't support editing outside of `<body>` element.
|
||||
return aLimitInBodyElement != LimitInBodyElement::Yes ||
|
||||
(document->GetBodyElement() &&
|
||||
nsContentUtils::ContentIsFlattenedTreeDescendantOf(
|
||||
candidateEditingHost, document->GetBodyElement()))
|
||||
? candidateEditingHost
|
||||
: document->GetBodyElement();
|
||||
}
|
||||
|
||||
void HTMLEditor::NotifyEditingHostMaybeChanged() {
|
||||
|
@ -6408,7 +6420,7 @@ nsresult HTMLEditor::GetPreferredIMEState(IMEState* aState) {
|
|||
}
|
||||
|
||||
already_AddRefed<Element> HTMLEditor::GetInputEventTargetElement() const {
|
||||
RefPtr<Element> target = GetActiveEditingHost();
|
||||
RefPtr<Element> target = GetActiveEditingHost(LimitInBodyElement::No);
|
||||
if (target) {
|
||||
return target.forget();
|
||||
}
|
||||
|
|
|
@ -631,7 +631,9 @@ class HTMLEditor final : public TextEditor,
|
|||
* Get an active editor's editing host in DOM window. If this editor isn't
|
||||
* active in the DOM window, this returns NULL.
|
||||
*/
|
||||
Element* GetActiveEditingHost() const;
|
||||
enum class LimitInBodyElement { No, Yes };
|
||||
Element* GetActiveEditingHost(
|
||||
LimitInBodyElement aLimitInBodyElement = LimitInBodyElement::Yes) const;
|
||||
|
||||
/**
|
||||
* Retruns true if we're in designMode.
|
||||
|
|
|
@ -1033,6 +1033,12 @@ nsresult HTMLEditor::ComputeTargetRanges(
|
|||
AutoRangeArray& aRangesToDelete) {
|
||||
MOZ_ASSERT(IsEditActionDataAvailable());
|
||||
|
||||
Element* editingHost = GetActiveEditingHost();
|
||||
if (!editingHost) {
|
||||
aRangesToDelete.RemoveAllRanges();
|
||||
return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
|
||||
}
|
||||
|
||||
// First check for table selection mode. If so, hand off to table editor.
|
||||
SelectedTableCellScanner scanner(aRangesToDelete);
|
||||
if (scanner.IsInTableCellSelectionMode()) {
|
||||
|
@ -1047,6 +1053,7 @@ nsresult HTMLEditor::ComputeTargetRanges(
|
|||
if (HTMLEditUtils::GetTableCellElementIfOnlyOneSelected(
|
||||
aRangesToDelete.Ranges()[i - removedRanges]) !=
|
||||
scanner.ElementsRef()[i]) {
|
||||
// XXX Need to manage anchor-focus range too!
|
||||
aRangesToDelete.Ranges().RemoveElementAt(i - removedRanges);
|
||||
removedRanges++;
|
||||
}
|
||||
|
@ -1054,7 +1061,14 @@ nsresult HTMLEditor::ComputeTargetRanges(
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
aRangesToDelete.EnsureOnlyEditableRanges(*editingHost);
|
||||
if (aRangesToDelete.Ranges().IsEmpty()) {
|
||||
NS_WARNING(
|
||||
"There is no range which we can delete entire of or around the caret");
|
||||
return NS_ERROR_EDITOR_NO_EDITABLE_RANGE;
|
||||
}
|
||||
AutoDeleteRangesHandler deleteHandler;
|
||||
// Should we delete target ranges which cannot delete actually?
|
||||
nsresult rv = deleteHandler.ComputeRangesToDelete(*this, aDirectionAndAmount,
|
||||
aRangesToDelete);
|
||||
NS_WARNING_ASSERTION(
|
||||
|
@ -1071,7 +1085,12 @@ EditActionResult HTMLEditor::HandleDeleteSelection(
|
|||
aStripWrappers == nsIEditor::eNoStrip);
|
||||
|
||||
if (!SelectionRefPtr()->RangeCount()) {
|
||||
return EditActionCanceled();
|
||||
return EditActionHandled(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
|
||||
}
|
||||
|
||||
Element* editingHost = GetActiveEditingHost();
|
||||
if (!editingHost) {
|
||||
return EditActionHandled(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
|
||||
}
|
||||
|
||||
// Remember that we did a selection deletion. Used by
|
||||
|
@ -1097,6 +1116,13 @@ EditActionResult HTMLEditor::HandleDeleteSelection(
|
|||
}
|
||||
|
||||
AutoRangeArray rangesToDelete(*SelectionRefPtr());
|
||||
rangesToDelete.EnsureOnlyEditableRanges(*editingHost);
|
||||
if (rangesToDelete.Ranges().IsEmpty()) {
|
||||
NS_WARNING(
|
||||
"There is no range which we can delete entire the ranges or around the "
|
||||
"caret");
|
||||
return EditActionHandled(NS_ERROR_EDITOR_NO_EDITABLE_RANGE);
|
||||
}
|
||||
AutoDeleteRangesHandler deleteHandler;
|
||||
EditActionResult result = deleteHandler.Run(*this, aDirectionAndAmount,
|
||||
aStripWrappers, rangesToDelete);
|
||||
|
|
|
@ -27,7 +27,7 @@ load 499844-1.html
|
|||
load 503709-1.xhtml
|
||||
load 513375-1.xhtml
|
||||
load 535632-1.xhtml
|
||||
load 574558-1.xhtml
|
||||
asserts(1) load 574558-1.xhtml # assertion in constructor of TextFragmentData (initialized for non-editable content)
|
||||
load 580151-1.xhtml
|
||||
load 582138-1.xhtml
|
||||
load 612565-1.html
|
||||
|
@ -82,7 +82,7 @@ load 1348851.html
|
|||
load 1350772.html
|
||||
load 1364133.html
|
||||
load 1366176.html
|
||||
asserts(2) load 1375131.html # assertion in WSRunScanner::GetEditableBlockParentOrTopmostEditableInlineContent()
|
||||
load 1375131.html
|
||||
load 1381541.html
|
||||
load 1383747.html
|
||||
load 1383755.html
|
||||
|
@ -138,4 +138,4 @@ load 1659717.html
|
|||
load 1663725.html # throws
|
||||
load 1655508.html
|
||||
pref(dom.document.exec_command.nested_calls_allowed,true) load 1666556.html
|
||||
asserts(2) load 1677566.html # assertion in constructor of TextFragmentData (initialized for non-editable content)
|
||||
asserts(1) load 1677566.html # assertion in constructor of TextFragmentData (initialized for non-editable content)
|
||||
|
|
|
@ -69,10 +69,8 @@ async function runTests() {
|
|||
|
||||
// Root element never can be edit target. If the editTarget is the root
|
||||
// element, replace with its body.
|
||||
let isEditTargetIsDescendantOfEditingHost = false;
|
||||
if (editTarget == aDocument.documentElement) {
|
||||
editTarget = body;
|
||||
isEditTargetIsDescendantOfEditingHost = true;
|
||||
}
|
||||
|
||||
editTarget.innerHTML = "";
|
||||
|
@ -397,9 +395,7 @@ async function runTests() {
|
|||
synthesizeKey("KEY_Enter", {}, aWindow);
|
||||
},
|
||||
{
|
||||
innerHTML: !isEditTargetIsDescendantOfEditingHost
|
||||
? "<div>B</div><div><br></div>"
|
||||
: "B<br><br>",
|
||||
innerHTML: "<div>B</div><div><br></div>",
|
||||
beforeInputEvent: {
|
||||
cancelable: true,
|
||||
inputType: "insertParagraph",
|
||||
|
@ -423,13 +419,8 @@ async function runTests() {
|
|||
});
|
||||
|
||||
function test_typing_C_in_empty_last_line(aTestData) {
|
||||
if (!isEditTargetIsDescendantOfEditingHost) {
|
||||
editTarget.innerHTML = "<div>B</div><div><br></div>";
|
||||
selection.collapse(editTarget.querySelector("div + div"), 0);
|
||||
} else {
|
||||
editTarget.innerHTML = "B<br><br>";
|
||||
selection.collapse(editTarget, 2);
|
||||
}
|
||||
editTarget.innerHTML = "<div>B</div><div><br></div>";
|
||||
selection.collapse(editTarget.querySelector("div + div"), 0);
|
||||
|
||||
runTest(
|
||||
aTestData,
|
||||
|
@ -437,9 +428,7 @@ async function runTests() {
|
|||
synthesizeKey("C", {shiftKey: true}, aWindow);
|
||||
},
|
||||
{
|
||||
innerHTML: !isEditTargetIsDescendantOfEditingHost
|
||||
? "<div>B</div><div>C<br></div>"
|
||||
: "B<br>C<br>",
|
||||
innerHTML: "<div>B</div><div>C<br></div>",
|
||||
beforeInputEvent: {
|
||||
cancelable: true,
|
||||
inputType: "insertText",
|
||||
|
@ -463,13 +452,8 @@ async function runTests() {
|
|||
});
|
||||
|
||||
function test_typing_enter_in_non_empty_last_line(aTestData) {
|
||||
if (!isEditTargetIsDescendantOfEditingHost) {
|
||||
editTarget.innerHTML = "<div>B</div><div>C<br></div>";
|
||||
selection.collapse(editTarget.querySelector("div + div").firstChild, 1);
|
||||
} else {
|
||||
editTarget.innerHTML = "B<br>C<br>";
|
||||
selection.collapse(editTarget.querySelector("br").nextSibling, 1);
|
||||
}
|
||||
editTarget.innerHTML = "<div>B</div><div>C<br></div>";
|
||||
selection.collapse(editTarget.querySelector("div + div").firstChild, 1);
|
||||
|
||||
runTest(
|
||||
aTestData,
|
||||
|
@ -477,9 +461,7 @@ async function runTests() {
|
|||
synthesizeKey("KEY_Enter", {}, aWindow);
|
||||
},
|
||||
{
|
||||
innerHTML: !isEditTargetIsDescendantOfEditingHost
|
||||
? "<div>B</div><div>C</div><div><br></div>"
|
||||
: "B<br>C<br><br>",
|
||||
innerHTML: "<div>B</div><div>C</div><div><br></div>",
|
||||
beforeInputEvent: {
|
||||
cancelable: true,
|
||||
inputType: "insertParagraph",
|
||||
|
|
|
@ -1,24 +1,4 @@
|
|||
[select-all-and-delete-in-html-element-having-contenteditable.html]
|
||||
[Select All, then, Backspace]
|
||||
expected:
|
||||
if (os == "linux"): FAIL
|
||||
PASS
|
||||
|
||||
[Select All, then, Delete]
|
||||
expected:
|
||||
if (os == "linux"): FAIL
|
||||
PASS
|
||||
|
||||
[Select All, then, execCommand("forwarddelete")]
|
||||
expected:
|
||||
if (os == "linux"): FAIL
|
||||
PASS
|
||||
|
||||
[Select All, then, execCommand("delete")]
|
||||
expected:
|
||||
if (os == "linux"): FAIL
|
||||
PASS
|
||||
|
||||
[getSelection().selectAllChildren(document.documentElement), then, Backspace]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -0,0 +1,598 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>InputEvent.getTargetRanges() of deleting a range across editing host boundaries</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";
|
||||
|
||||
// This test just check whether the deleted content range(s) and target ranges of `beforeinput`
|
||||
// are match or different. The behavior should be defined by editing API.
|
||||
// https://github.com/w3c/editing/issues/283
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest('<p>ab[c<span contenteditable="false">no]n-editable</span>def</p>');
|
||||
await sendBackspaceKey();
|
||||
const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>def</p>';
|
||||
const kOnlyEditableTextDeletedCase = '<p>ab<span contenteditable="false">non-editable</span>def</p>';
|
||||
const kNonEditableElementDeleteCase = '<p>abdef</p>';
|
||||
if (gEditor.innerHTML === kNothingDeletedCase) {
|
||||
if (gBeforeinput.length === 0) {
|
||||
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
|
||||
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
|
||||
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
|
||||
getRangeDescription(gBeforeinput[0].cachedRanges[0])
|
||||
})`);
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
|
||||
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
|
||||
assert_equals(gInput.length, 0,
|
||||
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kOnlyEditableTextDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild,
|
||||
startOffset: 2,
|
||||
endContainer: gEditor.firstChild.firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If only editable text is deleted, its target range should be the deleted text range");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If only editable text is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If only editable text is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeleteCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
assert_in_array(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
[
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild,
|
||||
startOffset: 2,
|
||||
endContainer: gEditor.firstChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild,
|
||||
startOffset: 2,
|
||||
endContainer: gEditor.firstChild.firstChild.nextSibling,
|
||||
endOffset: 0,
|
||||
}),
|
||||
],
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
assert_in_array(gEditor.innerHTML,
|
||||
[
|
||||
kNothingDeletedCase,
|
||||
kOnlyEditableTextDeletedCase,
|
||||
kNonEditableElementDeleteCase,
|
||||
], "The result content is unexpected");
|
||||
}, 'Backspace at "<p>ab[c<span contenteditable="false">no]n-editable</span>def</p>"');
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest('<p>abc<span contenteditable="false">non-[editable</span>de]f</p>');
|
||||
await sendBackspaceKey();
|
||||
const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>def</p>';
|
||||
const kOnlyEditableTextDeletedCase = '<p>abc<span contenteditable="false">non-editable</span>f</p>';
|
||||
const kNonEditableElementDeletedCase = '<p>abcf</p>';;
|
||||
if (gEditor.innerHTML === kNothingDeletedCase) {
|
||||
if (gBeforeinput.length === 0) {
|
||||
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
|
||||
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
|
||||
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
|
||||
getRangeDescription(gBeforeinput[0].cachedRanges[0])
|
||||
})`);
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
|
||||
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
|
||||
assert_equals(gInput.length, 0,
|
||||
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kOnlyEditableTextDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.lastChild,
|
||||
startOffset: 0,
|
||||
endContainer: gEditor.firstChild.lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If only editable text is deleted, its target range should be the deleted text range");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If only editable text is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If only editable text is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
assert_in_array(gEditor.innerHTML,
|
||||
[
|
||||
kNothingDeletedCase,
|
||||
kOnlyEditableTextDeletedCase,
|
||||
kNonEditableElementDeletedCase,
|
||||
], "The result content is unexpected");
|
||||
}, 'Backspace at "<p>abc<span contenteditable="false">non-[editable</span>de]f</p>"');
|
||||
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest('<p contenteditable="false"><span contenteditable>a[bc</span>non-editable<span contenteditable>de]f</span></p>');
|
||||
let firstRange = gSelection.getRangeAt(0);
|
||||
if (!firstRange ||
|
||||
firstRange.startContainer != gEditor.firstChild.firstChild.firstChild ||
|
||||
firstRange.startOffset != 1 ||
|
||||
firstRange.endContainer != gEditor.firstChild.lastChild.firstChild ||
|
||||
firstRange.endOffset != 2) {
|
||||
assert_true(true, "Selection couldn't set across editing host boundaries");
|
||||
return;
|
||||
}
|
||||
await sendBackspaceKey();
|
||||
const kNothingDeletedCase = '<p contenteditable="false"><span contenteditable="">abc</span>non-editable<span contenteditable="">def</span></p>';
|
||||
const kOnlyEditableContentDeletedCase = '<p contenteditable="false"><span contenteditable="">a</span>non-editable<span contenteditable="">f</span></p>';
|
||||
const kNonEditableElementDeletedCase = '<p contenteditable="false"><span contenteditable="">af</span></p>';
|
||||
const kDeleteEditableContentBeforeNonEditableContentCase = '<p contenteditable="false"><span contenteditable="">a</span>non-editable<span contenteditable="">def</span></p>';
|
||||
const kDeleteEditableContentAfterNonEditableContentCase = '<p contenteditable="false"><span contenteditable="">abc</span>non-editable<span contenteditable="">f</span></p>';
|
||||
if (gEditor.innerHTML === kNothingDeletedCase) {
|
||||
if (gBeforeinput.length === 0) {
|
||||
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
|
||||
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
|
||||
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
|
||||
getRangeDescription(gBeforeinput[0].cachedRanges[0])
|
||||
})`);
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
|
||||
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
|
||||
assert_equals(gInput.length, 0,
|
||||
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 2,
|
||||
"If only editable text is deleted, `beforeinput` event should have 2 target ranges");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.lastChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If only editable text is deleted, its first target range should be the deleted text range in the first text node");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.last.firstChild,
|
||||
startOffset: 0,
|
||||
endContainer: gEditor.firstChild.last.firstChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If only editable text is deleted, its second target range should be the deleted text range in the last text node");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If only editable text is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If only editable text is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.lastChild.firstChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text before non-editable element is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.firstChild.firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If editable text before non-editable element is deleted, its target range should be only the deleted text");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text before non-editable element is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text before non-editable element is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text after non-editable element is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.lastChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.lastChild.firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If editable text after non-editable element is deleted, its target range should be only the deleted text");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text after non-editable element is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text after non-editable element is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
assert_in_array(gEditor.innerHTML,
|
||||
[
|
||||
kNothingDeletedCase,
|
||||
kOnlyEditableContentDeletedCase,
|
||||
kNonEditableElementDeletedCase,
|
||||
kDeleteEditableContentBeforeNonEditableContentCase,
|
||||
kDeleteEditableContentAfterNonEditableContentCase,
|
||||
], "The result content is unexpected");
|
||||
}, 'Backspace at "<p contenteditable="false"><span contenteditable>a[bc</span>non-editable<span contenteditable>de]f</span></p>"');
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest('<p>a[bc<span contenteditable="false">non-editable<span contenteditable>de]f</span></span></p>');
|
||||
let firstRange = gSelection.getRangeAt(0);
|
||||
if (!firstRange ||
|
||||
firstRange.startContainer != gEditor.firstChild.firstChild ||
|
||||
firstRange.startOffset != 1 ||
|
||||
firstRange.endContainer != gEditor.querySelector("span span").firstChild ||
|
||||
firstRange.endOffset != 2) {
|
||||
assert_true(true, "Selection couldn't set across editing host boundaries");
|
||||
return;
|
||||
}
|
||||
await sendBackspaceKey();
|
||||
const kNothingDeletedCase = '<p>abc<span contenteditable="false">non-editable<span contenteditable="">def</span></span></p>';
|
||||
const kOnlyEditableContentDeletedCase = '<p>a<span contenteditable="false">non-editable<span contenteditable="">f</span></span></p>';
|
||||
const kNonEditableElementDeletedCase1 = '<p>af</p>';
|
||||
const kNonEditableElementDeletedCase2 = '<p>a<span contenteditable="">f</span></p>';
|
||||
const kDeleteEditableContentBeforeNonEditableContentCase ='<p>a<span contenteditable="false">non-editable<span contenteditable="">def</span></span></p>';
|
||||
const kDeleteEditableContentAfterNonEditableContentCase ='<p>abc<span contenteditable="false">non-editable<span contenteditable="">f</span></span></p>';
|
||||
if (gEditor.innerHTML === kNothingDeletedCase) {
|
||||
if (gBeforeinput.length === 0) {
|
||||
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
|
||||
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
|
||||
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
|
||||
getRangeDescription(gBeforeinput[0].cachedRanges[0])
|
||||
})`);
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
|
||||
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
|
||||
assert_equals(gInput.length, 0,
|
||||
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 2,
|
||||
"If only editable text is deleted, `beforeinput` event should have 2 target ranges");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If only editable text is deleted, its first target range should be the deleted text range in the first text node");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.querySelector("span span").firstChild,
|
||||
startOffset: 0,
|
||||
endContainer: gEditor.querySelector("span span").firstChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If only editable text is deleted, its second target range should be the deleted text range in the last text node");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If only editable text is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If only editable text is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeletedCase1) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
// XXX If the text nodes are merged, we need to cache it for here.
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeletedCase2) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.querySelector("span").firstChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text before non-editable element is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If editable text before non-editable element is deleted, its target range should be only the deleted text");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text before non-editable element is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text before non-editable element is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text after non-editable element is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.querySelector("span").firstChild,
|
||||
startOffset: 0,
|
||||
endContainer: gEditor.querySelector("span").firstChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text after non-editable element is deleted, its target range should be only the deleted text");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text after non-editable element is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text after non-editable element is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
assert_in_array(gEditor.innerHTML,
|
||||
[
|
||||
kNothingDeletedCase,
|
||||
kOnlyEditableContentDeletedCase,
|
||||
kNonEditableElementDeletedCase1,
|
||||
kNonEditableElementDeletedCase2,
|
||||
kDeleteEditableContentBeforeNonEditableContentCase,
|
||||
kDeleteEditableContentAfterNonEditableContentCase,
|
||||
], "The result content is unexpected");
|
||||
}, 'Backspace at "<p>a[bc<span contenteditable="false">non-editable<span contenteditable>de]f</span></span></p>"');
|
||||
|
||||
promise_test(async () => {
|
||||
initializeTest('<p><span contenteditable="false"><span contenteditable>a[bc</span>non-editable</span>de]f</p>');
|
||||
let firstRange = gSelection.getRangeAt(0);
|
||||
if (!firstRange ||
|
||||
firstRange.startContainer != gEditor.querySelector("span span").firstChild ||
|
||||
firstRange.startOffset != 1 ||
|
||||
firstRange.endContainer != gEditor.firstChild.lastChild.firstChild ||
|
||||
firstRange.endOffset != 2) {
|
||||
assert_true(true, "Selection couldn't set across editing host boundaries");
|
||||
return;
|
||||
}
|
||||
await sendBackspaceKey();
|
||||
const kNothingDeletedCase = '<p><span contenteditable="false"><span contenteditable="">abc</span>non-editable</span>def</p>';
|
||||
const kOnlyEditableContentDeletedCase = '<p><span contenteditable="false"><span contenteditable="">a</span>non-editable</span>f</p>';
|
||||
const kNonEditableElementDeletedCase1 = '<p><span contenteditable="false"><span contenteditable="">af</span></span></p>';
|
||||
const kNonEditableElementDeletedCase2 = '<p><span contenteditable="false"><span contenteditable="">a</span></span>f</p>';
|
||||
const kDeleteEditableContentBeforeNonEditableContentCase = '<p><span contenteditable="false"><span contenteditable="">a</span>non-editable</span>def</p>';
|
||||
const kDeleteEditableContentAfterNonEditableContentCase = '<p><span contenteditable="false"><span contenteditable="">abc</span>non-editable</span>f</p>';
|
||||
if (gEditor.innerHTML === kNothingDeletedCase) {
|
||||
if (gBeforeinput.length === 0) {
|
||||
assert_true(true, "If nothing changed, `beforeinput` event may not be fired");
|
||||
assert_equals(gInput.length, 0, "If nothing changed, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If nothing changed, `beforeinput` event can be fired for web apps can handle by themselves");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 0,
|
||||
`If nothing changed but \`beforeinput\` event is fired, its target range should be empty array (got ${
|
||||
getRangeDescription(gBeforeinput[0].cachedRanges[0])
|
||||
})`);
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContentBackward",
|
||||
"If nothing changed but `beforeinput` event is fired, its input type should be deleteContentBackward");
|
||||
assert_equals(gInput.length, 0,
|
||||
"If nothing changed but `beforeinput` event is fired, `input` event should not be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kOnlyEditableContentDeletedCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If only editable text is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 2,
|
||||
"If only editable text is deleted, `beforeinput` event should have 2 target ranges");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.querySelector("span span").firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.querySelector("span span").firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If only editable text is deleted, its first target range should be the deleted text range in the first text node");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[1]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.lastChild,
|
||||
startOffset: 0,
|
||||
endContainer: gEditor.firstChild.lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If only editable text is deleted, its second target range should be the deleted text range in the last text node");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If only editable text is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If only editable text is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeletedCase1) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
// XXX If the text nodes are merged, we need to cache it for here.
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.querySelector("span span").firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.querySelector("span span").lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kNonEditableElementDeletedCase2) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text and non-editable element are deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.querySelector("span span").firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.firstChild.lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text and non-editable element are deleted, its target range should include the deleted non-editable element");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text and non-editable element are deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text and non-editable element are deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kDeleteEditableContentBeforeNonEditableContentCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text before non-editable element is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text before non-editable element is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.querySelector("span span").firstChild,
|
||||
startOffset: 1,
|
||||
endContainer: gEditor.querySelector("span span").firstChild,
|
||||
endOffset: 3,
|
||||
}),
|
||||
"If editable text before non-editable element is deleted, its target range should be only the deleted text");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text before non-editable element is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text before non-editable element is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
if (gEditor.innerHTML === kDeleteEditableContentAfterNonEditableContentCase) {
|
||||
assert_equals(gBeforeinput.length, 1,
|
||||
"If editable text after non-editable element is deleted, `beforeinput` event should be fired");
|
||||
assert_equals(gBeforeinput[0].cachedRanges.length, 1,
|
||||
"If editable text after non-editable element is deleted, `beforeinput` event should have a target range");
|
||||
assert_equals(getRangeDescription(gBeforeinput[0].cachedRanges[0]),
|
||||
getRangeDescription({
|
||||
startContainer: gEditor.firstChild.lastChild,
|
||||
startOffset: 0,
|
||||
endContainer: gEditor.firstChild.lastChild,
|
||||
endOffset: 2,
|
||||
}),
|
||||
"If editable text after non-editable element is deleted, its target range should be only the deleted text");
|
||||
assert_equals(gBeforeinput[0].inputType, "deleteContent",
|
||||
"If editable text after non-editable element is deleted, its input type should be deleteContent");
|
||||
assert_equals(gInput.length, 1,
|
||||
"If editable text after non-editable element is deleted, `input` event should be fired");
|
||||
return;
|
||||
}
|
||||
assert_in_array(gEditor.innerHTML,
|
||||
[
|
||||
kNothingDeletedCase,
|
||||
kOnlyEditableContentDeletedCase,
|
||||
kNonEditableElementDeletedCase1,
|
||||
kNonEditableElementDeletedCase2,
|
||||
kDeleteEditableContentBeforeNonEditableContentCase,
|
||||
kDeleteEditableContentAfterNonEditableContentCase,
|
||||
], "The result content is unexpected");
|
||||
}, 'Backspace at "<p><span contenteditable="false"><span contenteditable>a[bc</span>non-editable</span>de]f</p>"');
|
||||
|
||||
</script>
|
|
@ -829,6 +829,11 @@ with modules["EDITOR"]:
|
|||
# and may keep handling the operation unexpectedly.
|
||||
errors["NS_ERROR_EDITOR_ACTION_CANCELED"] = FAILURE(3)
|
||||
|
||||
# An error code that indicates that there is no editable selection ranges.
|
||||
# E.g., Selection has no range, caret is in non-editable element,
|
||||
# non-collapsed range crosses editing host boundaries.
|
||||
errors["NS_ERROR_EDITOR_NO_EDITABLE_RANGE"] = FAILURE(4)
|
||||
|
||||
errors["NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND"] = SUCCESS(1)
|
||||
errors["NS_SUCCESS_EDITOR_FOUND_TARGET"] = SUCCESS(2)
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче