| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>InputEvent: beforeinput for Drag and Drop</title> |
| <script src="../../../resources/testharness.js"></script> |
| <script src="../../../resources/testharnessreport.js"></script> |
| <style> |
| div { |
| width: 100px; |
| height: 100px; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="editable1" contenteditable><b id="boldtext">EditableText</b></div> |
| <div id="editable2" contenteditable></div> |
| <textarea id="textarea1">Text</textarea> |
| <textarea id="barrier"></textarea> |
| <input id="input_drag"> |
| <input id="input_drop"> |
| <script> |
| function simulateDragDrop(dragElement, dropElement, start, end) { |
| if (dragElement.select) { |
| if (start === undefined || end === undefined) { |
| dragElement.select(); |
| } else { |
| dragElement.focus(); |
| dragElement.setSelectionRange(start, end); |
| } |
| } else { |
| var selection = window.getSelection(); |
| selection.collapse(dragElement, 0); |
| selection.extend(dragElement, 1); |
| } |
| |
| eventSender.mouseMoveTo(dragElement.offsetLeft + dragElement.offsetWidth / 2, |
| dragElement.offsetTop + dragElement.offsetHeight / 2); |
| eventSender.mouseDown(); |
| eventSender.leapForward(600); |
| eventSender.mouseMoveTo(dropElement.offsetLeft + dropElement.offsetWidth / 2, |
| dropElement.offsetTop + dropElement.offsetHeight / 2); |
| eventSender.mouseUp(); |
| } |
| |
| function assertCleanInitialDOM(logInfo) { |
| const editable1 = document.getElementById('editable1'); |
| const editable2 = document.getElementById('editable2'); |
| const textarea1 = document.getElementById('textarea1'); |
| assert_equals(editable1.children.length, 1, `${logInfo}, DOM is dirty`); |
| assert_equals(editable1.children[0].innerHTML, 'EditableText', `${logInfo}, DOM is dirty`); |
| assert_equals(editable2.children.length, 0, `${logInfo}, DOM is dirty`); |
| assert_equals(textarea1.value, 'Text', `${logInfo}, DOM is dirty`); |
| } |
| |
| test(function() { |
| assertCleanInitialDOM(); |
| assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.'); |
| assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.'); |
| |
| const editable1 = document.getElementById('editable1'); |
| const editable2 = document.getElementById('editable2'); |
| const textarea1 = document.getElementById('textarea1'); |
| |
| function preventDeleteByDragListener(event) { |
| if (event.inputType == 'deleteByDrag') |
| event.preventDefault(); |
| } |
| |
| function preventInsertFromDropListener(event) { |
| if (event.inputType == 'insertFromDrop') |
| event.preventDefault(); |
| } |
| |
| const undoBarrier = document.getElementById('barrier'); |
| undoBarrier.focus(); |
| document.execCommand('insertText', false, 'abc'); |
| function assertBarrierUnchanged(log) { |
| assert_equals(undoBarrier.value, 'abc', log); |
| } |
| |
| // Normally Drag&Drop requires a single Undo. |
| simulateDragDrop(editable1, editable2); |
| assert_equals(editable1.children.length, 0, `Normal Drag&Drop should remove data from editable1.`); |
| assert_equals(editable2.children.length, 1, `Normal Drag&Drop should insert data into editable2.`); |
| testRunner.execCommand('undo'); |
| assertCleanInitialDOM('Normal Drag&Drop'); |
| assertBarrierUnchanged('step 1'); |
| |
| // Canceling |DeleteByDrag|, still require a single Undo. |
| editable1.addEventListener('beforeinput', preventDeleteByDragListener); |
| simulateDragDrop(editable1, editable2); |
| testRunner.execCommand('undo'); |
| assertCleanInitialDOM('Canceling |DeleteByDrag|'); |
| editable1.removeEventListener('beforeinput', preventDeleteByDragListener); |
| assertBarrierUnchanged('step 2'); |
| |
| // Canceling |InsertFromDrop|, still require a single Undo. |
| editable2.addEventListener('beforeinput', preventInsertFromDropListener); |
| simulateDragDrop(editable1, editable2); |
| testRunner.execCommand('undo'); |
| assertCleanInitialDOM('Canceling |InsertFromDrop|'); |
| editable2.removeEventListener('beforeinput', preventInsertFromDropListener); |
| assertBarrierUnchanged('step 3'); |
| |
| // Canceling both, shouldn't create undo entry. |
| editable1.addEventListener('beforeinput', preventDeleteByDragListener); |
| editable2.addEventListener('beforeinput', preventInsertFromDropListener); |
| simulateDragDrop(editable1, editable2); |
| assertCleanInitialDOM('Canceling both'); |
| testRunner.execCommand('undo'); |
| assert_equals(undoBarrier.value, ''); |
| testRunner.execCommand('redo'); |
| assertBarrierUnchanged('step 4'); |
| editable1.removeEventListener('beforeinput', preventDeleteByDragListener); |
| editable2.removeEventListener('beforeinput', preventInsertFromDropListener); |
| |
| // Two Drag&Drop, cancel first |InsertFromDrop| and second |DeleteByDrag|, should still create 2 undo entries. |
| editable2.addEventListener('beforeinput', preventInsertFromDropListener); |
| simulateDragDrop(editable1, editable2); |
| editable2.removeEventListener('beforeinput', preventInsertFromDropListener); |
| textarea1.addEventListener('beforeinput', preventDeleteByDragListener); |
| textarea1.select(); |
| simulateDragDrop(textarea1, editable2); |
| textarea1.removeEventListener('beforeinput', preventDeleteByDragListener); |
| assert_equals(editable1.children.length, 0); |
| assert_equals(editable2.innerHTML, 'Text'); |
| assert_equals(textarea1.value, 'Text'); |
| // First undo. |
| testRunner.execCommand('undo'); |
| assert_equals(editable1.children.length, 0); |
| assert_equals(editable2.innerHTML, ''); |
| assert_equals(textarea1.value, 'Text'); |
| // Second undo. |
| testRunner.execCommand('undo'); |
| assert_equals(editable1.children.length, 1); |
| assert_equals(editable2.innerHTML, ''); |
| assert_equals(textarea1.value, 'Text'); |
| // More undo should reach to |undoBarrier|. |
| assertBarrierUnchanged('step 5'); |
| testRunner.execCommand('undo'); |
| assert_equals(undoBarrier.value, ''); |
| testRunner.execCommand('redo'); |
| }, 'Testing Drag and Drop, preventDefault() and Undo entry'); |
| |
| test(function() { |
| assertCleanInitialDOM(); |
| assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.'); |
| assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.'); |
| |
| const editable1 = document.getElementById('editable1'); |
| const editable2 = document.getElementById('editable2'); |
| var eventOrderRecorder = []; |
| document.addEventListener('beforeinput', event => |
| eventOrderRecorder.push(`beforeinput:${event.target.id}:${event.inputType}`)); |
| document.addEventListener('input', event => |
| eventOrderRecorder.push(`input:${event.target.id}:${event.inputType}`)); |
| ['drop', 'dragend'].forEach(eventType => document.addEventListener( |
| eventType, () => eventOrderRecorder.push(`${event.target.id}:${eventType}`))); |
| |
| function testDragDropEventOrder(dragElement, dropElement, expectedOrder) { |
| assert_equals(dragElement.children.length, 1); |
| eventOrderRecorder = []; |
| simulateDragDrop(dragElement, dropElement); |
| assert_array_equals(eventOrderRecorder, expectedOrder, |
| `Testing drag ${dragElement.id} onto ${dropElement.id} actual order: ${eventOrderRecorder}`); |
| } |
| |
| // Test Drag and Drop. |
| testDragDropEventOrder(editable1, editable2, |
| ['editable2:drop', 'beforeinput:boldtext:deleteByDrag', 'input:editable1:deleteByDrag', |
| 'beforeinput:editable2:insertFromDrop', 'input:editable2:insertFromDrop', 'editable1:dragend']); |
| testRunner.execCommand('undo'); |
| }, 'Testing Drag and Drop event order'); |
| |
| test(function() { |
| assertCleanInitialDOM(); |
| assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.'); |
| assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.'); |
| |
| const editable1 = document.getElementById('editable1'); |
| const editable2 = document.getElementById('editable2'); |
| var lastPlainTextData = {}; |
| var lastHTMLData = {}; |
| document.addEventListener('beforeinput', event => { |
| lastPlainTextData[event.inputType] = event.dataTransfer ? event.dataTransfer.getData('text/plain') : null; |
| lastHTMLData[event.inputType] = event.dataTransfer ? event.dataTransfer.getData('text/html') : null; |
| }); |
| |
| function testDragDropDataTransfer(inputType, dragElement, dropElement, expectedPlainText, expectedHTML) { |
| assert_equals(dragElement.children.length, 1); |
| lastPlainTextData = {}; |
| lastHTMLData = {}; |
| simulateDragDrop(dragElement, dropElement); |
| assert_equals(lastPlainTextData[inputType], expectedPlainText, |
| `Testing '${inputType}' getData('text/plain')`); |
| if (expectedHTML && expectedHTML.test) { |
| assert_regexp_match(lastHTMLData[inputType], expectedHTML, |
| `Testing '${inputType}' getData('text/html')`); |
| } else { |
| assert_equals(lastHTMLData[inputType], expectedHTML, |
| `Testing '${inputType}' getData('text/html')`); |
| } |
| } |
| |
| // Test Drag and Drop. |
| testDragDropDataTransfer('deleteByDrag', editable1, editable2, null, null); |
| testRunner.execCommand('undo'); |
| testDragDropDataTransfer('insertFromDrop', editable1, editable2, 'EditableText', /^.*EditableText<\/b>$/); |
| testRunner.execCommand('undo'); |
| }, 'Testing Drag and Drop dataTransfer'); |
| |
| test(function() { |
| assertCleanInitialDOM(); |
| assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.'); |
| assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.'); |
| |
| const editable1 = document.getElementById('editable1'); |
| const editable2 = document.getElementById('editable2'); |
| var inputTypesToPrevent = []; |
| document.addEventListener('beforeinput', event => { |
| if (inputTypesToPrevent.indexOf(event.inputType) != -1) |
| event.preventDefault(); |
| }); |
| |
| function testDragDropPreventDefault(preventDefaultTypes, dragElement, dropElement, expectedDragElementChildren, expectedDropElementChildren) { |
| assert_equals(dragElement.children.length, 1); |
| inputTypesToPrevent = preventDefaultTypes; |
| simulateDragDrop(dragElement, dropElement); |
| assert_equals(dragElement.children.length, expectedDragElementChildren, |
| 'Testing preventDefault() on ${preventDefaultTypes} ${dragElement.id} children count'); |
| assert_equals(dropElement.children.length, expectedDropElementChildren, |
| 'Testing preventDefault() on ${preventDefaultTypes} ${dropElement.id} children count'); |
| inputTypesToPrevent = []; |
| } |
| |
| // Preventing single 'beforeinput' will only cancel DOM update for one event, |
| // the remaining DOM update will still update undo stack. |
| testDragDropPreventDefault(['deleteByDrag'], editable1, editable2, 1, 1); |
| testRunner.execCommand('undo'); |
| testDragDropPreventDefault(['insertFromDrop'], editable1, editable2, 0, 0); |
| testRunner.execCommand('undo'); |
| |
| // Adding 'insertHTML' command to undo stack. |
| editable2.focus(); |
| document.execCommand('insertHTML', false, '<b>B</b><i>i</i>'); |
| assert_equals(editable2.children.length, 2, |
| '"editable2" should have 2 children after "insertHTML" command'); |
| // Canceling both |deleteByDrag| and |insertFromDrop| won't modify undo stack. |
| testDragDropPreventDefault(['deleteByDrag', 'insertFromDrop'], editable1, editable2, 1, 2); |
| // |undo| will undo last 'insertHTML' command. |
| testRunner.execCommand('undo'); |
| assert_equals(editable2.children.length, 0, |
| '"editable2" should have 0 children after undo "insertHTML"'); |
| }, 'Testing Drag and Drop preventDefault()'); |
| |
| test(function() { |
| assertCleanInitialDOM(); |
| assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.'); |
| assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.'); |
| |
| const editable1 = document.getElementById('editable1'); |
| const editable2 = document.getElementById('editable2'); |
| var eventOrderRecorder = []; |
| [editable1, editable2].forEach(editable => { |
| editable.addEventListener('beforeinput', event => |
| eventOrderRecorder.push(`beforeinput:${editable.id}:${event.inputType}`)); |
| editable.addEventListener('input', event => |
| eventOrderRecorder.push(`input:${editable.id}:${event.inputType}`)); |
| editable.addEventListener('drop', event => |
| eventOrderRecorder.push(`${editable.id}:drop`)); |
| editable.addEventListener('dragend', event => |
| eventOrderRecorder.push(`${editable.id}:dragend`)); |
| }); |
| |
| function testDragDropEventOrder(dragElement, dropElement, expectedOrder) { |
| assert_equals(dragElement.children.length, 1); |
| eventOrderRecorder = []; |
| simulateDragDrop(dragElement, dropElement); |
| assert_array_equals(eventOrderRecorder, expectedOrder, |
| `Testing drag ${dragElement.id} onto ${dropElement.id} actual order: ${eventOrderRecorder}`); |
| } |
| |
| function removeEditable1Listener() { |
| editable1.remove(); |
| } |
| |
| function removeEditable2Listener() { |
| editable2.remove(); |
| } |
| |
| // Testing remove drop target, |editable2| won't get 'beforeinput' as it's disconnected. |
| editable1.addEventListener('beforeinput', removeEditable2Listener); |
| testDragDropEventOrder(editable1, editable2, |
| ['editable2:drop', 'beforeinput:editable1:deleteByDrag', 'input:editable1:deleteByDrag', 'editable1:dragend']); |
| editable1.removeEventListener('beforeinput', removeEditable2Listener); |
| testRunner.execCommand('undo'); |
| document.body.appendChild(editable2); |
| |
| // Testing remove drag target, |editable1| won't receive DOM updates after disconnected. |
| editable1.addEventListener('beforeinput', removeEditable1Listener); |
| testDragDropEventOrder(editable1, editable2, |
| ['editable2:drop', 'beforeinput:editable1:deleteByDrag', 'beforeinput:editable2:insertFromDrop', |
| 'input:editable2:insertFromDrop', 'editable1:dragend']); |
| editable1.removeEventListener('beforeinput', removeEditable1Listener); |
| testRunner.execCommand('undo'); |
| document.body.appendChild(editable1); |
| }, 'Testing element removed by event handler'); |
| |
| test(() => { |
| assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.'); |
| assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.'); |
| |
| const input_drag = document.getElementById('input_drag'); |
| const input_drop = document.getElementById('input_drop'); |
| |
| input_drag.value = '12345678'; |
| input_drop.value = '4'; |
| |
| simulateDragDrop(input_drag, input_drop, 1, 8); |
| |
| assert_equals(input_drag.value, '1'); |
| assert_equals(input_drop.value, '42345678'); |
| |
| testRunner.execCommand('undo'); |
| |
| assert_equals(input_drag.value, '12345678'); |
| assert_equals(input_drop.value, '4'); |
| }, 'Undo drag&drop should update input.value'); |
| </script> |
| </body> |
| </html> |