<!doctype html> <html> <head> <link rel="stylesheet" href="/tests/SimpleTest/test.css"> <script src="/tests/SimpleTest/SimpleTest.js"></script> <script src="/tests/SimpleTest/EventUtils.js"></script> </head> <body> <div id="dropZone" ondragenter="event.dataTransfer.dropEffect = 'copy'; event.preventDefault();" ondragover="event.dataTransfer.dropEffect = 'copy'; event.preventDefault();" ondrop="event.preventDefault();" style="height: 4px; background-color: lemonchiffon;"></div> <div id="container"></div> <script type="application/javascript"> SimpleTest.waitForExplicitFinish(); function checkInputEvent(aEvent, aExpectedTarget, aInputType, aData, aDataTransfer, aTargetRanges, aDescription) { ok(aEvent instanceof InputEvent, `${aDescription}: "${aEvent.type}" event should be dispatched with InputEvent interface`); is(aEvent.cancelable, aEvent.type === "beforeinput", `${aDescription}: "${aEvent.type}" event should be ${aEvent.type === "beforeinput" ? "" : "never "}cancelable`); is(aEvent.bubbles, true, `${aDescription}: "${aEvent.type}" event should always bubble`); is(aEvent.target, aExpectedTarget, `${aDescription}: "${aEvent.type}" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element`); is(aEvent.inputType, aInputType, `${aDescription}: inputType of "${aEvent.type}" event should be "${aInputType}" on the <${aExpectedTarget.tagName.toLowerCase()}> element`); is(aEvent.data, aData, `${aDescription}: data of "${aEvent.type}" event should be ${aData} on the <${aExpectedTarget.tagName.toLowerCase()}> element`); if (aDataTransfer === null) { is(aEvent.dataTransfer, null, `${aDescription}: dataTransfer should be null on the <${aExpectedTarget.tagName.toLowerCase()}> element`); } else { for (let dataTransfer of aDataTransfer) { let description = `${aDescription}: on the <${aExpectedTarget.tagName.toLowerCase()}> element`; if (dataTransfer.todo) { // XXX It seems that synthesizeDrop() don't emulate perfectly if caller specifies the data directly. todo_is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`); } else { is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`); } } } let targetRanges = aEvent.getTargetRanges(); if (aTargetRanges.length === 0) { is(targetRanges.length, 0, `${aDescription}: getTargetRange() of "${aEvent.type}" event should return empty array`); } else { is(targetRanges.length, aTargetRanges.length, `${aDescription}: getTargetRange() of "${aEvent.type}" event should return static range array`); if (targetRanges.length == aTargetRanges.length) { for (let i = 0; i < targetRanges.length; i++) { is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, `${aDescription}: startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, `${aDescription}: startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, `${aDescription}: endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, `${aDescription}: endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); } } } } // eslint-disable-next-line complexity async function doTest() { const container = document.getElementById("container"); const dropZone = document.getElementById("dropZone"); let beforeinputEvents = []; let inputEvents = []; let dragEvents = []; function onBeforeinput(event) { beforeinputEvents.push(event); } function onInput(event) { inputEvents.push(event); } document.addEventListener("beforeinput", onBeforeinput); document.addEventListener("input", onInput); function preventDefaultDeleteByDrag(aEvent) { if (aEvent.inputType === "deleteByDrag") { aEvent.preventDefault(); } } function preventDefaultInsertFromDrop(aEvent) { if (aEvent.inputType === "insertFromDrop") { aEvent.preventDefault(); } } const selection = window.getSelection(); const kIsMac = navigator.platform.includes("Mac"); const kIsWin = navigator.platform.includes("Win"); const kNativeLF = kIsWin ? "\r\n" : "\n"; const kModifiersToCopy = { ctrlKey: !kIsMac, altKey: kIsMac, } function comparePlainText(aGot, aExpected, aDescription) { is(aGot.replace(/\r\n?/g, "\n"), aExpected, aDescription); } function compareHTML(aGot, aExpected, aDescription) { is(aGot.replace(/\r\n?/g, "\n"), aExpected, aDescription); } async function trySynthesizePlainDragAndDrop(aDescription, aOptions) { try { await synthesizePlainDragAndDrop(aOptions); return true; } catch (e) { ok(false, `${aDescription}: Failed to emulate drag and drop (${e.message})`); return false; } } // -------- Test dragging regular text await (async function test_dragging_regular_text() { const description = "dragging part of non-editable <span> element"; container.innerHTML = '<span style="font-size: 24px;">Some Text</span>'; const span = document.querySelector("div#container > span"); selection.setBaseAndExtent(span.firstChild, 4, span.firstChild, 6); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), span.textContent.substring(4, 6), `${description}: dataTransfer should have selected text as "text/plain"`); compareHTML(aEvent.dataTransfer.getData("text/html"), span.outerHTML.replace(/>.+</, `>${span.textContent.substring(4, 6)}<`), `${description}: dataTransfer should have the parent inline element and only selected text as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging non-editable selection to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging non-editable selection to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from an <input> await (async function test_dragging_text_from_input_element() { const description = "dragging part of text in <input> element"; container.innerHTML = '<input value="Drag Me">'; const input = document.querySelector("div#container > input"); document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. input.setSelectionRange(1, 4); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(1, 4), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should not have data as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from an <textarea> await (async function test_dragging_text_from_textarea_element() { const description = "dragging part of text in <textarea> element"; container.innerHTML = "<textarea>Some Text To Drag</textarea>"; const textarea = document.querySelector("div#container > textarea"); document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. textarea.setSelectionRange(1, 7); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(1, 7), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should not have data as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from a contenteditable await (async function test_dragging_text_from_contenteditable() { const description = "dragging part of text in contenteditable element"; container.innerHTML = "<p contenteditable>This is some <b>editable</b> text.</p>"; const b = document.querySelector("div#container > p > b"); selection.setBaseAndExtent(b.firstChild, 2, b.firstChild, 6); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), b.textContent.substring(2, 6), `${description}: dataTransfer should have selected text as "text/plain"`); compareHTML(aEvent.dataTransfer.getData("text/html"), b.outerHTML.replace(/>.+</, `>${b.textContent.substring(2, 6)}<`), `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("drop", onDrop); })(); for (const inputType of ["text", "search"]) { // -------- Test dragging regular text of text/html to <input> await (async function test_dragging_text_from_span_element_to_input_element() { const description = `dragging text in non-editable <span> to <input type=${inputType}>`; container.innerHTML = `<span>Static</span><input type="${inputType}">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), span.textContent.substring(2, 5), `${description}: dataTransfer should have selected text as "text/plain"`); compareHTML(aEvent.dataTransfer.getData("text/html"), span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(input.value, span.textContent.substring(2, 5), `${description}: <input>.value should be modified`); is(beforeinputEvents.length, 1, `${description}: one "beforeinput" event should be fired on <input>`); checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); is(inputEvents.length, 1, `${description}: one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <input>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging regular text of text/html to disabled <input> await (async function test_dragging_text_from_span_element_to_disabled_input_element() { const description = `dragging text in non-editable <span> to <input disabled type="${inputType}">`; container.innerHTML = `<span>Static</span><input disabled type="${inputType}">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(input.value, "", `${description}: <input disable>.value should not be modified`); is(beforeinputEvents.length, 0, `${description}: no "beforeinput" event should be fired on <input disabled>`); is(inputEvents.length, 0, `${description}: no "input" event should be fired on <input disabled>`); is(dragEvents.length, 0, `${description}: no "drop" event should be fired on <input disabled>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging regular text of text/html to readonly <input> await (async function test_dragging_text_from_span_element_to_readonly_input_element() { const description = `dragging text in non-editable <span> to <input readonly type="${inputType}">`; container.innerHTML = `<span>Static</span><input readonly type="${inputType}">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), span.textContent.substring(2, 5), `${description}: dataTransfer should have selected text as "text/plain"`); compareHTML(aEvent.dataTransfer.getData("text/html"), span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(input.value, "", `${description}: <input readonly>.value should not be modified`); is(beforeinputEvents.length, 0, `${description}: no "beforeinput" event should be fired on <input readonly>`); is(inputEvents.length, 0, `${description}: no "input" event should be fired on <input readonly>`); is(dragEvents.length, 0, `${description}: no "drop" event should be fired on <input readonly>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging only text/html data (like from another app) to <input>. await (async function test_dragging_only_html_text_to_input_element() { const description = `dragging only text/html data to <input type="${inputType}>`; container.innerHTML = `<span>Static</span><input type="${inputType}">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.selectAllChildren(span); beforeinputEvents = []; inputEvents = []; const onDragStart = aEvent => { // Clear all dataTransfer data first. Then, it'll be filled only with // the text/html data passed to synthesizeDrop(). aEvent.dataTransfer.clearData(); }; window.addEventListener("dragstart", onDragStart, {capture: true}); synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy"); is(beforeinputEvents.length, 0, `${description}: no "beforeinput" event should be fired on <input>`); is(inputEvents.length, 0, `${description}: no "input" event should be fired on <input>`); window.removeEventListener("dragstart", onDragStart, {capture: true}); })(); // -------- Test dragging both text/plain and text/html data (like from another app) to <input>. await (async function test_dragging_both_html_text_and_plain_text_to_input_element() { const description = `dragging both text/plain and text/html data to <input type=${inputType}>`; container.innerHTML = `<span>Static</span><input type="${inputType}">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.selectAllChildren(span); beforeinputEvents = []; inputEvents = []; const onDragStart = aEvent => { // Clear all dataTransfer data first. Then, it'll be filled only with // the text/plain data and text/html data passed to synthesizeDrop(). aEvent.dataTransfer.clearData(); }; window.addEventListener("dragstart", onDragStart, {capture: true}); synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}, {type: "text/plain", data: "Some Plain Text"}]], "copy"); is(input.value, "Some Plain Text", `${description}: The text/plain data should be inserted`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on <input> element`); checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "Some Plain Text", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on <input> element`); checkInputEvent(inputEvents[0], input, "insertFromDrop", "Some Plain Text", null, [], description); window.removeEventListener("dragstart", onDragStart, {capture: true}); })(); // -------- Test dragging special text type from another app to <input> await (async function test_dragging_only_moz_text_internal_to_input_element() { const description = `dragging both text/x-moz-text-internal data to <input type="${inputType}">`; container.innerHTML = `<span>Static</span><input type="${inputType}">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.selectAllChildren(span); beforeinputEvents = []; inputEvents = []; const onDragStart = aEvent => { // Clear all dataTransfer data first. Then, it'll be filled only with // the text/x-moz-text-internal data passed to synthesizeDrop(). aEvent.dataTransfer.clearData(); }; window.addEventListener("dragstart", onDragStart, {capture: true}); synthesizeDrop(span, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy"); is(input.value, "", `${description}: <input>.value should not be modified with "text/x-moz-text-internal" data`); // Note that even if editor does not handle given dataTransfer, web apps // may handle it by itself. Therefore, editor should dispatch "beforeinput" // event. is(beforeinputEvents.length, 1, `${description}: one "beforeinput" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`); // But unfortunately, on <input> and <textarea>, dataTransfer won't be set... checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "", null, [], description); is(inputEvents.length, 0, `${description}: no "input" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`); window.removeEventListener("dragstart", onDragStart, {capture: true}); })(); // -------- Test dragging contenteditable to <input> await (async function test_dragging_from_contenteditable_to_input_element() { const description = `dragging text in contenteditable to <input type="${inputType}">`; container.innerHTML = `<div contenteditable>Some <b>bold</b> text</div><input type="${inputType}">`; const contenteditable = document.querySelector("div#container > div"); const input = document.querySelector("div#container > input"); const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "me bold t", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(contenteditable.innerHTML, "Soext", `${description}: Dragged range should be removed from contenteditable`); is(input.value, "me bold t", `${description}: <input>.value should be modified`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 2, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on contenteditable and <input>`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to <input> (canceling "deleteByDrag") await (async function test_dragging_from_contenteditable_to_input_element_and_canceling_delete_by_drag() { const description = `dragging text in contenteditable to <input type="${inputType}"> (canceling "deleteByDrag")`; container.innerHTML = `<div contenteditable>Some <b>bold</b> text</div><input type="${inputType}">`; const contenteditable = document.querySelector("div#container > div"); const input = document.querySelector("div#container > input"); const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "me bold t", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(contenteditable.innerHTML, "Some <b>bold</b> text", `${description}: Dragged range shouldn't be removed from contenteditable`); is(input.value, "me bold t", `${description}: <input>.value should be modified`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 2, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "insertFromDrop", "me bold t", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <input>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging contenteditable to <input> (canceling "insertFromDrop") await (async function test_dragging_from_contenteditable_to_input_element_and_canceling_insert_from_drop() { const description = `dragging text in contenteditable to <input type="${inputType}"> (canceling "insertFromDrop")`; container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><input>"; const contenteditable = document.querySelector("div#container > div"); const input = document.querySelector("div#container > input"); const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "me bold t", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(contenteditable.innerHTML, "Soext", `${description}: Dragged range should be removed from contenteditable`); is(input.value, "", `${description}: <input>.value shouldn't be modified`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 2, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <input>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); } // -------- Test dragging regular text of text/html to <input type="number"> // // FIXME(emilio): The -moz-appearance bit is just a hack to // work around bug 1611720. await (async function test_dragging_from_span_element_to_input_element_whose_type_number() { const description = `dragging text in non-editable <span> to <input type="number">`; container.innerHTML = `<span>123456</span><input type="number" style="-moz-appearance: textfield">`; const span = document.querySelector("div#container > span"); const input = document.querySelector("div#container > input"); selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), span.textContent.substring(2, 5), `${description}: dataTransfer should have selected text as "text/plain"`); compareHTML(aEvent.dataTransfer.getData("text/html"), span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(input.value, span.textContent.substring(2, 5), `${description}: <input>.value should be modified`); is(beforeinputEvents.length, 1, `${description}: one "beforeinput" event should be fired on <input>`); checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); is(inputEvents.length, 1, `${description}: one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <input>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging only text/plain data (like from another app) to contenteditable. await (async function test_dragging_only_plain_text_to_contenteditable() { const description = "dragging both text/plain and text/html data to contenteditable"; container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>'; const span = document.querySelector("div#container > span"); const contenteditable = document.querySelector("div#container > div"); selection.selectAllChildren(span); beforeinputEvents = []; inputEvents = []; const onDragStart = aEvent => { // Clear all dataTransfer data first. Then, it'll be filled only with // the text/plain data and text/html data passed to synthesizeDrop(). aEvent.dataTransfer.clearData(); }; window.addEventListener("dragstart", onDragStart, {capture: true}); synthesizeDrop(span, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy"); is(contenteditable.innerHTML, "Sample Text", `${description}: The text/plain data should be inserted`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable element`); checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, [{todo: true, type: "text/plain", data: "Sample Text"}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable element`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{todo: true, type: "text/plain", data: "Sample Text"}], [], description); window.removeEventListener("dragstart", onDragStart, {capture: true}); })(); // -------- Test dragging only text/html data (like from another app) to contenteditable. await (async function test_dragging_only_html_text_to_contenteditable() { const description = "dragging only text/html data to contenteditable"; container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>'; const span = document.querySelector("div#container > span"); const contenteditable = document.querySelector("div#container > div"); selection.selectAllChildren(span); beforeinputEvents = []; inputEvents = []; const onDragStart = aEvent => { // Clear all dataTransfer data first. Then, it'll be filled only with // the text/plain data and text/html data passed to synthesizeDrop(). aEvent.dataTransfer.clearData(); }; window.addEventListener("dragstart", onDragStart, {capture: true}); synthesizeDrop(span, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy"); is(contenteditable.innerHTML, "Sample <i>Italic</i> Text", `${description}: The text/plain data should be inserted`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable element`); checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable element`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}], [], description); window.removeEventListener("dragstart", onDragStart, {capture: true}); })(); // -------- Test dragging regular text of text/plain to <textarea> await (async function test_dragging_from_span_element_to_textarea_element() { const description = "dragging text in non-editable <span> to <textarea>"; container.innerHTML = "<span>Static</span><textarea></textarea>"; const span = document.querySelector("div#container > span"); const textarea = document.querySelector("div#container > textarea"); selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), span.textContent.substring(2, 5), `${description}: dataTransfer should have selected text as "text/plain"`); compareHTML(aEvent.dataTransfer.getData("text/html"), span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { is(textarea.value, span.textContent.substring(2, 5), `${description}: <textarea>.value should be modified`); is(beforeinputEvents.length, 1, `${description}: one "beforeinput" event should be fired on <textarea>`); checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); is(inputEvents.length, 1, `${description}: one "input" event should be fired on <textarea>`); checkInputEvent(inputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to <textarea> await (async function test_dragging_contenteditable_to_textarea_element() { const description = "dragging text in contenteditable to <textarea>"; container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; const contenteditable = document.querySelector("div#container > div"); const textarea = document.querySelector("div#container > textarea"); const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "me bold t", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { is(contenteditable.innerHTML, "Soext", `${description}: Dragged range should be removed from contenteditable`); is(textarea.value, "me bold t", `${description}: <textarea>.value should be modified`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 2, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on contenteditable and <textarea>`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to <textarea> (canceling "deleteByDrag") await (async function test_dragging_from_contenteditable_to_textarea_and_canceling_delete_by_drag() { const description = 'dragging text in contenteditable to <textarea> (canceling "deleteByDrag")'; container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; const contenteditable = document.querySelector("div#container > div"); const textarea = document.querySelector("div#container > textarea"); const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "me bold t", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { is(contenteditable.innerHTML, "Some <b>bold</b> text", `${description}: Dragged range shouldn't be removed from contenteditable`); is(textarea.value, "me bold t", `${description}: <textarea>.value should be modified`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 2, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <textarea>`); checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "me bold t", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging contenteditable to <textarea> (canceling "insertFromDrop") await (async function test_dragging_from_contenteditable_to_textarea_and_canceling_insert_from_drop() { const description = 'dragging text in contenteditable to <textarea> (canceling "insertFromDrop")'; container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; const contenteditable = document.querySelector("div#container > div"); const textarea = document.querySelector("div#container > textarea"); const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "me bold t", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { is(contenteditable.innerHTML, "Soext", `${description}: Dragged range should be removed from contenteditable`); is(textarea.value, "", `${description}: <textarea>.value shouldn't be modified`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 2, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test dragging contenteditable to same contenteditable await (async function test_dragging_from_contenteditable_to_itself() { const description = "dragging text in contenteditable to same contenteditable"; container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const span = document.querySelector("div#container > div > span"); const lastTextNode = span.firstChild; const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: span, } ) ) { todo_is(contenteditable.innerHTML, "<b>bd</b> <span>MM<b>ol</b>MM</span>", `${description}: dragged range should be removed from contenteditable`); todo_isnot(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span><b>ol</b>", `${description}: dragged range should be removed from contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: lastTextNode, startOffset: 4, endContainer: lastTextNode, endOffset: 4}], // XXX unexpected description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to same contenteditable (canceling "deleteByDrag") await (async function test_dragging_from_contenteditable_to_itself_and_canceling_delete_by_drag() { const description = 'dragging text in contenteditable to same contenteditable (canceling "deleteByDrag")'; container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const span = document.querySelector("div#container > div > span"); const lastTextNode = span.firstChild; const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: span, } ) ) { todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>", `${description}: dragged range shouldn't be removed from contenteditable`); todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>", `${description}: dragged range shouldn't be removed from contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: lastTextNode, startOffset: 4, endContainer: lastTextNode, endOffset: 4}], // XXX unexpected description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging contenteditable to same contenteditable (canceling "insertFromDrop") await (async function test_dragging_from_contenteditable_to_itself_and_canceling_insert_from_drop() { const description = 'dragging text in contenteditable to same contenteditable (canceling "insertFromDrop")'; container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const span = document.querySelector("div#container > div > span"); const lastTextNode = span.firstChild; const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: span, } ) ) { is(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span>", `${description}: dragged range should be removed from contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: lastTextNode, startOffset: 4, endContainer: lastTextNode, endOffset: 4}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging contenteditable to same contenteditable await (async function test_copy_dragging_from_contenteditable_to_itself() { const description = "copy-dragging text in contenteditable to same contenteditable"; container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; document.documentElement.scrollTop; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const span = document.querySelector("div#container > div > span"); const lastTextNode = span.firstChild; selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: span, dragEvent: kModifiersToCopy, } ) ) { todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>", `${description}: dragged range shouldn't be removed from contenteditable`); todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>", `${description}: dragged range shouldn't be removed from contenteditable`); is(beforeinputEvents.length, 1, `${description}: only 1 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: lastTextNode, startOffset: 4, endContainer: lastTextNode, endOffset: 4}], // XXX unexpected description); is(inputEvents.length, 1, `${description}: only 1 "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to other contenteditable await (async function test_dragging_from_contenteditable_to_other_contenteditable() { const description = "dragging text in contenteditable to other contenteditable"; container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const otherContenteditable = document.querySelector("div#container > div ~ div"); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, } ) ) { is(contenteditable.innerHTML, "<b>bd</b>", `${description}: dragged range should be removed from contenteditable`); is(otherContenteditable.innerHTML, "<b>ol</b>", `${description}: dragged content should be inserted into other contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to other contenteditable (canceling "deleteByDrag") await (async function test_dragging_from_contenteditable_to_other_contenteditable_and_canceling_delete_by_drag() { const description = 'dragging text in contenteditable to other contenteditable (canceling "deleteByDrag")'; container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const otherContenteditable = document.querySelector("div#container > div ~ div"); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, } ) ) { is(contenteditable.innerHTML, "<b>bold</b>", `${description}: dragged range shouldn't be removed from contenteditable`); is(otherContenteditable.innerHTML, "<b>ol</b>", `${description}: dragged content should be inserted into other contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on other contenteditable`); checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging contenteditable to other contenteditable (canceling "insertFromDrop") await (async function test_dragging_from_contenteditable_to_other_contenteditable_and_canceling_insert_from_drop() { const description = 'dragging text in contenteditable to other contenteditable (canceling "insertFromDrop")'; container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const otherContenteditable = document.querySelector("div#container > div ~ div"); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, } ) ) { is(contenteditable.innerHTML, "<b>bd</b>", `${description}: dragged range should be removed from contenteditable`); is(otherContenteditable.innerHTML, "", `${description}: dragged content shouldn't be inserted into other contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging contenteditable to other contenteditable await (async function test_copy_dragging_from_contenteditable_to_other_contenteditable() { const description = "copy-dragging text in contenteditable to other contenteditable"; container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > b"); const otherContenteditable = document.querySelector("div#container > div ~ div"); selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, dragEvent: kModifiersToCopy, } ) ) { is(contenteditable.innerHTML, "<b>bold</b>", `${description}: dragged range shouldn't be removed from contenteditable`); is(otherContenteditable.innerHTML, "<b>ol</b>", `${description}: dragged content should be inserted into other contenteditable`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on other contenteditable`); checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on other contenteditable`); checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging nested contenteditable to contenteditable await (async function test_dragging_from_nested_contenteditable_to_contenteditable() { const description = "dragging text in nested contenteditable to contenteditable"; container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const otherContenteditable = document.querySelector("div#container > div > div > p"); const b = document.querySelector("div#container > div > div > p > b"); contenteditable.focus(); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: contenteditable.firstChild, } ) ) { is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>', `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: contenteditable.firstChild, startOffset: 0, endContainer: contenteditable.firstChild, endOffset: 0}], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging nested contenteditable to contenteditable (canceling "deleteByDrag") await (async function test_dragging_from_nested_contenteditable_to_contenteditable_canceling_delete_by_drag() { const description = 'dragging text in nested contenteditable to contenteditable (canceling "deleteByDrag")'; container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const otherContenteditable = document.querySelector("div#container > div > div > p"); const b = document.querySelector("div#container > div > div > p > b"); contenteditable.focus(); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: contenteditable.firstChild, } ) ) { is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>', `${description}: dragged range should be copied from nested contenteditable to the contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: contenteditable.firstChild, startOffset: 0, endContainer: contenteditable.firstChild, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging nested contenteditable to contenteditable (canceling "insertFromDrop") await (async function test_dragging_from_nested_contenteditable_to_contenteditable_canceling_insert_from_drop() { const description = 'dragging text in nested contenteditable to contenteditable (canceling "insertFromDrop")'; container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const otherContenteditable = document.querySelector("div#container > div > div > p"); const b = document.querySelector("div#container > div > div > p > b"); contenteditable.focus(); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: contenteditable.firstChild, } ) ) { is(contenteditable.innerHTML, '<p><br></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>', `${description}: dragged range should be removed from nested contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: contenteditable.firstChild, startOffset: 0, endContainer: contenteditable.firstChild, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on nested contenteditable`); checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging nested contenteditable to contenteditable await (async function test_copy_dragging_from_nested_contenteditable_to_contenteditable() { const description = "copy-dragging text in nested contenteditable to contenteditable"; container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > div > p > b"); contenteditable.focus(); selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: contenteditable.firstChild, dragEvent: kModifiersToCopy, } ) ) { is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>', `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: contenteditable.firstChild, startOffset: 0, endContainer: contenteditable.firstChild, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to nested contenteditable await (async function test_dragging_from_contenteditable_to_nested_contenteditable() { const description = "dragging text in contenteditable to nested contenteditable"; container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > p > b"); const otherContenteditable = document.querySelector("div#container > div > div > p"); contenteditable.focus(); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, } ) ) { is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', `${description}: dragged range should be moved from contenteditable to nested contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on contenteditable and nested contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging contenteditable to nested contenteditable (canceling "deleteByDrag") await (async function test_dragging_from_contenteditable_to_nested_contenteditable_and_canceling_delete_by_drag() { const description = 'dragging text in contenteditable to nested contenteditable (canceling "deleteByDrag")'; container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > p > b"); const otherContenteditable = document.querySelector("div#container > div > div > p"); contenteditable.focus(); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, } ) ) { is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', `${description}: dragged range should be copied from contenteditable to nested contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable and nested contenteditable`); checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging contenteditable to nested contenteditable (canceling "insertFromDrop") await (async function test_dragging_from_contenteditable_to_nested_contenteditable_and_canceling_insert_from_drop() { const description = 'dragging text in contenteditable to nested contenteditable (canceling "insertFromDrop")'; container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > p > b"); const otherContenteditable = document.querySelector("div#container > div > div > p"); contenteditable.focus(); const selectionContainers = [b.firstChild, b.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, } ) ) { is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><br></p></div>', `${description}: dragged range should be removed from contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 1, endContainer: selectionContainers[1], endOffset: 3}], description); checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging contenteditable to nested contenteditable await (async function test_copy_dragging_from_contenteditable_to_nested_contenteditable() { const description = "copy-dragging text in contenteditable to nested contenteditable"; container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; const contenteditable = document.querySelector("div#container > div"); const b = document.querySelector("div#container > div > p > b"); const otherContenteditable = document.querySelector("div#container > div > div > p"); contenteditable.focus(); selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); is(aEvent.dataTransfer.getData("text/plain"), "ol", `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", `${description}: dataTransfer should have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: otherContenteditable, dragEvent: kModifiersToCopy, } ) ) { is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [{startContainer: otherContenteditable, startOffset: 0, endContainer: otherContenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, [{type: "text/html", data: "<b>ol</b>"}, {type: "text/plain", data: "ol"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <input> to contenteditable await (async function test_dragging_from_input_element_to_contenteditable() { const description = "dragging text in <input> to contenteditable"; container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const contenteditable = document.querySelector("div#container > div"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: contenteditable, } ) ) { is(input.value, "Somt", `${description}: dragged range should be removed from <input>`); is(contenteditable.innerHTML, "e Tex<br>", `${description}: dragged content should be inserted into contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <input> and contenteditable`); checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <input> to contenteditable (canceling "deleteByDrag") await (async function test_dragging_from_input_element_to_contenteditable_and_canceling_delete_by_drag() { const description = 'dragging text in <input> to contenteditable (canceling "deleteByDrag")'; container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const contenteditable = document.querySelector("div#container > div"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: contenteditable, } ) ) { is(input.value, "Some Text", `${description}: dragged range shouldn't be removed from <input>`); is(contenteditable.innerHTML, "e Tex<br>", `${description}: dragged content should be inserted into contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging text in <input> to contenteditable (canceling "insertFromDrop") await (async function test_dragging_from_input_element_to_contenteditable_and_canceling_insert_from_drop() { const description = 'dragging text in <input> to contenteditable (canceling "insertFromDrop")'; container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const contenteditable = document.querySelector("div#container > div"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: contenteditable, } ) ) { is(input.value, "Somt", `${description}: dragged range should be removed from <input>`); is(contenteditable.innerHTML, "<br>", `${description}: dragged content shouldn't be inserted into contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging text in <input> to contenteditable await (async function test_copy_dragging_from_input_element_to_contenteditable() { const description = "copy-dragging text in <input> to contenteditable"; container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const contenteditable = document.querySelector("div#container > div"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: contenteditable, dragEvent: kModifiersToCopy, } ) ) { is(input.value, "Some Text", `${description}: dragged range shouldn't be removed from <input>`); is(contenteditable.innerHTML, "e Tex<br>", `${description}: dragged content should be inserted into contenteditable`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: "e Tex"}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <textarea> to contenteditable await (async function test_dragging_from_textarea_element_to_contenteditable() { const description = "dragging text in <textarea> to contenteditable"; container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const contenteditable = document.querySelector("div#container > div"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: contenteditable, } ) ) { is(textarea.value, "Linne2", `${description}: dragged range should be removed from <textarea>`); todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>", `${description}: dragged content should be inserted into contenteditable`); todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>", `${description}: dragged content should be inserted into contenteditable`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: `e1${kNativeLF}Li`}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <input> and contenteditable`); checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: `e1${kNativeLF}Li`}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test copy-dragging text in <textarea> to contenteditable await (async function test_copy_dragging_from_textarea_element_to_contenteditable() { const description = "copy-dragging text in <textarea> to contenteditable"; container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const contenteditable = document.querySelector("div#container > div"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: contenteditable, dragEvent: kModifiersToCopy, } ) ) { is(textarea.value, "Line1\nLine2", `${description}: dragged range should be removed from <textarea>`); todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>", `${description}: dragged content should be inserted into contenteditable`); todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>", `${description}: dragged content should be inserted into contenteditable`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: `e1${kNativeLF}Li`}], [{startContainer: contenteditable, startOffset: 0, endContainer: contenteditable, endOffset: 0}], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, [{type: "text/plain", data: `e1${kNativeLF}Li`}], [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <input> to other <input> await (async function test_dragging_from_input_element_to_other_input_element() { const description = "dragging text in <input> to other <input>"; container.innerHTML = '<input value="Some Text"><input>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const otherInput = document.querySelector("div#container > input + input"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: otherInput, } ) ) { is(input.value, "Somt", `${description}: dragged range should be removed from <input>`); is(otherInput.value, "e Tex", `${description}: dragged content should be inserted into other <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <input> and other <input>`); checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other <input>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <input> to other <input> (canceling "deleteByDrag") await (async function test_dragging_from_input_element_to_other_input_element_and_canceling_delete_by_drag() { const description = 'dragging text in <input> to other <input> (canceling "deleteByDrag")'; container.innerHTML = '<input value="Some Text"><input>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const otherInput = document.querySelector("div#container > input + input"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: otherInput, } ) ) { is(input.value, "Some Text", `${description}: dragged range shouldn't be removed from <input>`); is(otherInput.value, "e Tex", `${description}: dragged content should be inserted into other <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on other <input>`); checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other <input>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging text in <input> to other <input> (canceling "insertFromDrop") await (async function test_dragging_from_input_element_to_other_input_element_and_canceling_insert_from_drop() { const description = 'dragging text in <input> to other <input> (canceling "insertFromDrop")'; container.innerHTML = '<input value="Some Text"><input>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const otherInput = document.querySelector("div#container > input + input"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: otherInput, }, ) ) { is(input.value, "Somt", `${description}: dragged range should be removed from <input>`); is(otherInput.value, "", `${description}: dragged content shouldn't be inserted into other <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other <input>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging text in <input> to other <input> await (async function test_copy_dragging_from_input_element_to_other_input_element() { const description = "copy-dragging text in <input> to other <input>"; container.innerHTML = '<input value="Some Text"><input>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const otherInput = document.querySelector("div#container > input + input"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: otherInput, dragEvent: kModifiersToCopy, } ) ) { is(input.value, "Some Text", `${description}: dragged range shouldn't be removed from <input>`); is(otherInput.value, "e Tex", `${description}: dragged content should be inserted into other <input>`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on other <input>`); checkInputEvent(beforeinputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on other <input>`); checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other <input>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <input> to <textarea> await (async function test_dragging_from_input_element_to_textarea_element() { const description = "dragging text in <input> to other <textarea>"; container.innerHTML = '<input value="Some Text"><textarea></textarea>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const textarea = document.querySelector("div#container > textarea"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: textarea, } ) ) { is(input.value, "Somt", `${description}: dragged range should be removed from <input>`); is(textarea.value, "e Tex", `${description}: dragged content should be inserted into <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <input> and <textarea>`); checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <input> to <textarea> (canceling "deleteByDrag") await (async function test_dragging_from_input_element_to_textarea_element_and_canceling_delete_by_drag() { const description = 'dragging text in <input> to other <textarea> (canceling "deleteByDrag")'; container.innerHTML = '<input value="Some Text"><textarea></textarea>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const textarea = document.querySelector("div#container > textarea"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: textarea, } ) ) { is(input.value, "Some Text", `${description}: dragged range shouldn't be removed from <input>`); is(textarea.value, "e Tex", `${description}: dragged content should be inserted into <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <textarea>`); checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging text in <input> to <textarea> (canceling "insertFromDrop") await (async function test_dragging_from_input_element_to_textarea_element_and_canceling_insert_from_drop() { const description = 'dragging text in <input> to other <textarea> (canceling "insertFromDrop")'; container.innerHTML = '<input value="Some Text"><textarea></textarea>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const textarea = document.querySelector("div#container > textarea"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: textarea, } ) ) { is(input.value, "Somt", `${description}: dragged range should be removed from <input>`); is(textarea.value, "", `${description}: dragged content shouldn't be inserted into <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging text in <input> to <textarea> await (async function test_copy_dragging_from_input_element_to_textarea_element() { const description = "copy-dragging text in <input> to <textarea>"; container.innerHTML = '<input value="Some Text"><textarea></textarea>'; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const input = document.querySelector("div#container > input"); const textarea = document.querySelector("div#container > textarea"); input.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: textarea, dragEvent: kModifiersToCopy, } ) ) { is(input.value, "Some Text", `${description}: dragged range shouldn't be removed from <input>`); is(textarea.value, "e Tex", `${description}: dragged content should be inserted into <textarea>`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on <textarea>`); checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on <textarea>`); checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <textarea> to <input> await (async function test_dragging_from_textarea_element_to_input_element() { const description = "dragging text in <textarea> to <input>"; container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const input = document.querySelector("div#container > input"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: input, } ) ) { is(textarea.value, "Linne2", `${description}: dragged range should be removed from <textarea>`); is(input.value, "e1 Li", `${description}: dragged content should be inserted into <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <textarea> and <input>`); checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <textarea> to <input> (canceling "deleteByDrag") await (async function test_dragging_from_textarea_element_to_input_element_and_delete_by_drag() { const description = 'dragging text in <textarea> to <input> (canceling "deleteByDrag")'; container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const input = document.querySelector("div#container > input"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: input, } ) ) { is(textarea.value, "Line1\nLine2", `${description}: dragged range shouldn't be removed from <textarea>`); is(input.value, "e1 Li", `${description}: dragged content should be inserted into <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <input>`); checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging text in <textarea> to <input> (canceling "insertFromDrop") await (async function test_dragging_from_textarea_element_to_input_element_and_canceling_insert_from_drop() { const description = 'dragging text in <textarea> to <input> (canceling "insertFromDrop")'; container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const input = document.querySelector("div#container > input"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: input, } ) ) { is(textarea.value, "Linne2", `${description}: dragged range should be removed from <textarea>`); is(input.value, "", `${description}: dragged content shouldn't be inserted into <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <textarea>`); checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging text in <textarea> to <input> await (async function test_copy_dragging_from_textarea_element_to_input_element() { const description = "copy-dragging text in <textarea> to <input>"; container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const input = document.querySelector("div#container > input"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: input, dragEvent: kModifiersToCopy, } ) ) { is(textarea.value, "Line1\nLine2", `${description}: dragged range shouldn't be removed from <textarea>`); is(input.value, "e1 Li", `${description}: dragged content should be inserted into <input>`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on <input>`); checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on <input>`); checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <textarea> to other <textarea> await (async function test_dragging_from_textarea_element_to_other_textarea_element() { const description = "dragging text in <textarea> to other <textarea>"; container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const otherTextarea = document.querySelector("div#container > textarea + textarea"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: otherTextarea, } ) ) { is(textarea.value, "Linne2", `${description}: dragged range should be removed from <textarea>`); is(otherTextarea.value, "e1\nLi", `${description}: dragged content should be inserted into other <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <textarea> and other <textarea>`); checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text in <textarea> to other <textarea> (canceling "deleteByDrag") await (async function test_dragging_from_textarea_element_to_other_textarea_element_and_canceling_delete_by_drag() { const description = 'dragging text in <textarea> to other <textarea> (canceling "deleteByDrag")'; container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const otherTextarea = document.querySelector("div#container > textarea + textarea"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultDeleteByDrag); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: otherTextarea, } ) ) { is(textarea.value, "Line1\nLine2", `${description}: dragged range shouldn't be removed from <textarea>`); is(otherTextarea.value, "e1\nLi", `${description}: dragged content should be inserted into other <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on other <textarea>`); checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); })(); // -------- Test dragging text in <textarea> to other <textarea> (canceling "insertFromDrop") await (async function test_dragging_from_textarea_element_to_other_textarea_element_and_canceling_insert_from_drop() { const description = 'dragging text in <textarea> to other <textarea> (canceling "insertFromDrop")'; container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const otherTextarea = document.querySelector("div#container > textarea + textarea"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); document.addEventListener("beforeinput", preventDefaultInsertFromDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: otherTextarea, } ) ) { is(textarea.value, "Linne2", `${description}: dragged range should be removed from <textarea>`); is(otherTextarea.value, "", `${description}: dragged content shouldn't be inserted into other <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" event should be fired on <textarea>`); checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); })(); // -------- Test copy-dragging text in <textarea> to other <textarea> await (async function test_copy_dragging_from_textarea_element_to_other_textarea_element() { const description = "copy-dragging text in <textarea> to other <textarea>"; container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. const textarea = document.querySelector("div#container > textarea"); const otherTextarea = document.querySelector("div#container > textarea + textarea"); textarea.setSelectionRange(3, 8); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should have not have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: otherTextarea, dragEvent: kModifiersToCopy, } ) ) { is(textarea.value, "Line1\nLine2", `${description}: dragged range shouldn't be removed from <textarea>`); is(otherTextarea.value, "e1\nLi", `${description}: dragged content should be inserted into other <textarea>`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on other <textarea>`); checkInputEvent(beforeinputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on other <textarea>`); checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on <textarea>`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging multiple-line text in contenteditable to <input> await (async function test_dragging_multiple_line_text_in_contenteditable_to_input_element() { const description = "dragging multiple-line text in contenteditable to <input>"; container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>'; const contenteditable = document.querySelector("div#container > div"); const input = document.querySelector("div#container > input"); const selectionContainers = [contenteditable.firstChild.firstChild, contenteditable.firstChild.nextSibling.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 3, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", `${description}: dataTransfer should have have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, } ) ) { is(contenteditable.innerHTML, "<div>Linne2</div>", `${description}: dragged content should be removed from contenteditable`); is(input.value, "e1 Li", `${description}: dragged range should be inserted into <input>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 3, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <input> and contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test copy-dragging multiple-line text in contenteditable to <input> await (async function test_copy_dragging_multiple_line_text_in_contenteditable_to_input_element() { const description = "copy-dragging multiple-line text in contenteditable to <input>"; container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>'; const contenteditable = document.querySelector("div#container > div"); const input = document.querySelector("div#container > input"); selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3, contenteditable.firstChild.nextSibling.firstChild, 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", `${description}: dataTransfer should have have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: input, dragEvent: kModifiersToCopy, } ) ) { is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>", `${description}: dragged content should be removed from contenteditable`); is(input.value, "e1 Li", `${description}: dragged range should be inserted into <input>`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging multiple-line text in contenteditable to <textarea> await (async function test_dragging_multiple_line_text_in_contenteditable_to_textarea_element() { const description = "dragging multiple-line text in contenteditable to <textarea>"; container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>'; const contenteditable = document.querySelector("div#container > div"); const textarea = document.querySelector("div#container > textarea"); const selectionContainers = [contenteditable.firstChild.firstChild, contenteditable.firstChild.nextSibling.firstChild]; selection.setBaseAndExtent(selectionContainers[0], 3, selectionContainers[1], 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", `${description}: dataTransfer should have have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { is(contenteditable.innerHTML, "<div>Linne2</div>", `${description}: dragged content should be removed from contenteditable`); is(textarea.value, "e1\nLi", `${description}: dragged range should be inserted into <textarea>`); is(beforeinputEvents.length, 2, `${description}: 2 "beforeinput" events should be fired on <textarea> and contenteditable`); checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, [{startContainer: selectionContainers[0], startOffset: 3, endContainer: selectionContainers[1], endOffset: 2}], description); checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 2, `${description}: 2 "input" events should be fired on <textarea> and contenteditable`); checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); checkInputEvent(inputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test copy-dragging multiple-line text in contenteditable to <textarea> await (async function test_copy_dragging_multiple_line_text_in_contenteditable_to_textarea_element() { const description = "copy-dragging multiple-line text in contenteditable to <textarea>"; container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>'; const contenteditable = document.querySelector("div#container > div"); const textarea = document.querySelector("div#container > textarea"); selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3, contenteditable.firstChild.nextSibling.firstChild, 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", `${description}: dataTransfer should have have selected nodes as "text/html"`); }; document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, dragEvent: kModifiersToCopy, } ) ) { is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>", `${description}: dragged content should be removed from contenteditable`); is(textarea.value, "e1\nLi", `${description}: dragged range should be inserted into <textarea>`); is(beforeinputEvents.length, 1, `${description}: only one "beforeinput" events should be fired on contenteditable`); checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(inputEvents.length, 1, `${description}: only one "input" events should be fired on contenteditable`); checkInputEvent(inputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired on other contenteditable`); } document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from an <input> and reframing the <input> element before dragend. await (async function test_dragging_from_input_element_and_reframing_input_element() { const description = "dragging part of text in <input> element and reframing the <input> element before dragend"; container.innerHTML = '<input value="Drag Me">'; const input = document.querySelector("div#container > input"); document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. input.setSelectionRange(1, 4); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDragStart = () => { input.style.display = "none"; document.documentElement.scrollTop; input.style.display = ""; document.documentElement.scrollTop; }; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(1, 4), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should not have data as "text/html"`); }; document.addEventListener("dragStart", onDragStart); document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("dragStart", onDragStart); document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragend. await (async function test_dragging_from_textarea_element_and_reframing_textarea_element() { const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragend"; container.innerHTML = "<textarea>Some Text To Drag</textarea>"; const textarea = document.querySelector("div#container > textarea"); document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. textarea.setSelectionRange(1, 7); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDragStart = () => { textarea.style.display = "none"; document.documentElement.scrollTop; textarea.style.display = ""; document.documentElement.scrollTop; }; const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(1, 7), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should not have data as "text/html"`); }; document.addEventListener("dragStart", onDragStart); document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("dragStart", onDragStart); document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from an <input> and reframing the <input> element before dragstart. await (async function test_dragging_from_input_element_and_reframing_input_element_before_dragstart() { const description = "dragging part of text in <input> element and reframing the <input> element before dragstart"; container.innerHTML = '<input value="Drag Me">'; const input = document.querySelector("div#container > input"); document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. input.setSelectionRange(1, 4); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onMouseMove = () => { input.style.display = "none"; document.documentElement.scrollTop; input.style.display = ""; document.documentElement.scrollTop; }; const onMouseDown = () => { document.addEventListener("mousemove", onMouseMove, {once: true}); } const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(1, 4), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should not have data as "text/html"`); }; document.addEventListener("mousedown", onMouseDown, {once: true}); document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(input).editor.selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("drop", onDrop); })(); // -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragstart. await (async function test_dragging_from_textarea_element_and_reframing_textarea_element_before_dragstart() { const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragstart"; container.innerHTML = "<textarea>Some Text To Drag</textarea>"; const textarea = document.querySelector("div#container > textarea"); document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. textarea.setSelectionRange(1, 7); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onMouseMove = () => { textarea.style.display = "none"; document.documentElement.scrollTop; textarea.style.display = ""; document.documentElement.scrollTop; }; const onMouseDown = () => { document.addEventListener("mousemove", onMouseMove, {once: true}); } const onDrop = aEvent => { dragEvents.push(aEvent); comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(1, 7), `${description}: dataTransfer should have selected text as "text/plain"`); is(aEvent.dataTransfer.getData("text/html"), "", `${description}: dataTransfer should not have data as "text/html"`); }; document.addEventListener("mousedown", onMouseDown, {once: true}); document.addEventListener("drop", onDrop); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: SpecialPowers.wrap(textarea).editor.selection, destElement: dropZone, } ) ) { is(beforeinputEvents.length, 0, `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); is(inputEvents.length, 0, `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); is(dragEvents.length, 1, `${description}: only one "drop" event should be fired`); } document.removeEventListener("mousedown", onMouseDown); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("drop", onDrop); })(); await (async function test_dragend_when_left_half_of_text_node_dragged_into_textarea() { const description = "dragging left half of text in contenteditable into <textarea>"; container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; const editingHost = container.querySelector("[contenteditable]"); const textNode = editingHost.querySelector("p").firstChild; const textarea = container.querySelector("textarea"); selection.setBaseAndExtent(textNode, 0, textNode, textNode.length / 2); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDragEnd = aEvent => dragEvents.push(aEvent); document.addEventListener("dragend", onDragEnd, {capture: true}); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { ok( textNode.isConnected, `${description}: the text node part of whose text is dragged should not be removed` ); is( dragEvents.length, 1, `${description}: only one "dragend" event should be fired` ); is( dragEvents[0]?.target, textNode, `${description}: "dragend" should be fired on the text node which the mouse button down on` ); } document.removeEventListener("dragend", onDragEnd, {capture: true}); })(); await (async function test_dragend_when_right_half_of_text_node_dragged_into_textarea() { const description = "dragging right half of text in contenteditable into <textarea>"; container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; const editingHost = container.querySelector("[contenteditable]"); const textNode = editingHost.querySelector("p").firstChild; const textarea = container.querySelector("textarea"); selection.setBaseAndExtent(textNode, textNode.length / 2, textNode, textNode.length); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDragEnd = aEvent => dragEvents.push(aEvent); document.addEventListener("dragend", onDragEnd, {capture: true}); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { ok( textNode.isConnected, `${description}: the text node part of whose text is dragged should not be removed` ); is( dragEvents.length, 1, `${description}: only one "dragend" event should be fired` ); is( dragEvents[0]?.target, textNode, `${description}: "dragend" should be fired on the text node which the mouse button down on` ); } document.removeEventListener("dragend", onDragEnd, {capture: true}); })(); await (async function test_dragend_when_middle_part_of_text_node_dragged_into_textarea() { const description = "dragging middle of text in contenteditable into <textarea>"; container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; const editingHost = container.querySelector("[contenteditable]"); const textNode = editingHost.querySelector("p").firstChild; const textarea = container.querySelector("textarea"); selection.setBaseAndExtent(textNode, "ab".length, textNode, "abcd".length); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDragEnd = aEvent => dragEvents.push(aEvent); document.addEventListener("dragend", onDragEnd, {capture: true}); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { ok( textNode.isConnected, `${description}: the text node part of whose text is dragged should not be removed` ); is( dragEvents.length, 1, `${description}: only one "dragend" event should be fired` ); is( dragEvents[0]?.target, textNode, `${description}: "dragend" should be fired on the text node which the mouse button down on` ); } document.removeEventListener("dragend", onDragEnd, {capture: true}); })(); await (async function test_dragend_when_all_of_text_node_dragged_into_textarea() { const description = "dragging all of text in contenteditable into <textarea>"; container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; const editingHost = container.querySelector("[contenteditable]"); const textNode = editingHost.querySelector("p").firstChild; const textarea = container.querySelector("textarea"); selection.setBaseAndExtent(textNode, 0, textNode, textNode.length); beforeinputEvents = []; inputEvents = []; dragEvents = []; const onDragEnd = aEvent => dragEvents.push(aEvent); document.addEventListener("dragend", onDragEnd, {capture: true}); if ( await trySynthesizePlainDragAndDrop( description, { srcSelection: selection, destElement: textarea, } ) ) { ok( !textNode.isConnected, `${description}: the text node whose all text is dragged should've been removed from the contenteditable` ); is( dragEvents.length, 1, `${description}: only one "dragend" event should be fired` ); is( dragEvents[0]?.target, editingHost, `${description}: "dragend" should be fired on the editing host which is parent of the removed text node` ); } document.removeEventListener("dragend", onDragEnd, {capture: true}); })(); document.removeEventListener("beforeinput", onBeforeinput); document.removeEventListener("input", onInput); SimpleTest.finish(); } SimpleTest.waitForFocus(doTest); </script> </body> </html>