| <!DOCTYPE html> | 
 | <html> | 
 | <head> | 
 | <title>EditContext: The HTMLElement.editContext property</title> | 
 | <script src="/resources/testharness.js"></script> | 
 | <script src="/resources/testharnessreport.js"></script> | 
 | <script src="/resources/testdriver.js"></script> | 
 | <script src="/resources/testdriver-actions.js"></script> | 
 | <script src="/resources/testdriver-vendor.js"></script> | 
 | </head> | 
 | <body> | 
 |   <script> | 
 |       const kBackspaceKey = "\uE003"; | 
 |       const kDeleteKey = "\uE017"; | 
 |  | 
 |     async function testBasicTestInput(element) { | 
 |       const editContext = new EditContext(); | 
 |       let textForView = ""; | 
 |       document.body.appendChild(element); | 
 |       let beforeInputType = null; | 
 |       let beforeInputTargetRanges = null; | 
 |       element.addEventListener("beforeinput", e => { | 
 |         beforeInputType = e.inputType; | 
 |         beforeInputTargetRanges = e.getTargetRanges().map( | 
 |           staticRange => [staticRange.startOffset, staticRange.endOffset]); | 
 |       }); | 
 |       editContext.addEventListener("textupdate", e => { | 
 |         textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`; | 
 |       }); | 
 |       element.editContext = editContext; | 
 |       element.focus(); | 
 |       await test_driver.send_keys(element, 'a'); | 
 |       assert_equals(editContext.text, "a"); | 
 |       assert_equals(textForView, "a"); | 
 |       assert_equals(beforeInputType, "insertText"); | 
 |       if (element instanceof HTMLCanvasElement) { | 
 |         // DOM selection doesn't work inside <canvas>, so events | 
 |         // in <canvas> can't have target ranges. | 
 |         assert_equals(beforeInputTargetRanges.length, 0); | 
 |       } else { | 
 |         assert_equals(beforeInputTargetRanges.length, 1); | 
 |         assert_array_equals(beforeInputTargetRanges[0], [0, 0]); | 
 |       } | 
 |  | 
 |       element.remove(); | 
 |     } | 
 |  | 
 |     promise_test(testBasicTestInput.bind(null, document.createElement("div")), "Basic text input with div"); | 
 |     promise_test(testBasicTestInput.bind(null, document.createElement("canvas")), "Basic text input with canvas"); | 
 |  | 
 |     async function testBasicTestInputWithExistingSelection(element) { | 
 |       const editContext = new EditContext(); | 
 |       let textForView = ""; | 
 |       document.body.appendChild(element); | 
 |       editContext.addEventListener("textupdate", e => { | 
 |         textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`; | 
 |       }); | 
 |       element.editContext = editContext; | 
 |       element.focus(); | 
 |  | 
 |       editContext.updateText(0, 0, "abcd"); | 
 |       textForView = "abcd"; | 
 |       assert_equals(editContext.text, "abcd"); | 
 |       editContext.updateSelection(2, 3); | 
 |       await test_driver.send_keys(element, 'Z'); | 
 |       assert_equals(editContext.text, "abZd"); | 
 |       assert_equals(textForView, "abZd"); | 
 |  | 
 |       editContext.updateSelection(2, 1); | 
 |       await test_driver.send_keys(element, 'Y'); | 
 |       assert_equals(editContext.text, "aYZd"); | 
 |       assert_equals(textForView, "aYZd"); | 
 |  | 
 |       element.remove(); | 
 |     } | 
 |  | 
 |     promise_test(testBasicTestInputWithExistingSelection.bind(null, document.createElement("div")), "Text insertion with non-collapsed selection with div"); | 
 |     promise_test(testBasicTestInputWithExistingSelection.bind(null, document.createElement("canvas")), "Text insertion with non-collapsed selection with canvas"); | 
 |  | 
 |     promise_test(async function() { | 
 |       const editContext = new EditContext(); | 
 |       assert_not_equals(editContext, null); | 
 |       const test = document.createElement("div"); | 
 |       document.body.appendChild(test); | 
 |       test.editContext = editContext; | 
 |       test.focus(); | 
 |       await test_driver.send_keys(test, 'a'); | 
 |       assert_equals(test.innerHTML, ""); | 
 |       test.remove(); | 
 |     }, 'EditContext should disable DOM mutation'); | 
 |  | 
 |     promise_test(async function() { | 
 |       const editContext = new EditContext(); | 
 |       assert_not_equals(editContext, null); | 
 |       const test = document.createElement("div"); | 
 |       document.body.appendChild(test); | 
 |       test.focus(); | 
 |       test.editContext = editContext; | 
 |       test.addEventListener("beforeinput", e => { | 
 |         if (e.inputType === "insertText") { | 
 |           e.preventDefault(); | 
 |         } | 
 |       }); | 
 |       await test_driver.send_keys(test, 'a'); | 
 |       assert_equals(editContext.text, ""); | 
 |       test.remove(); | 
 |     }, 'beforeInput(insertText) should be cancelable'); | 
 |  | 
 |   promise_test(async () => { | 
 |     let div = document.createElement("div"); | 
 |     document.body.appendChild(div); | 
 |     let divText = "Hello World"; | 
 |     div.innerText = divText; | 
 |     div.editContext = new EditContext(); | 
 |     div.focus(); | 
 |     let got_before_input_event = false; | 
 |     div.addEventListener("beforeinput", e => { | 
 |       got_before_input_event = true; | 
 |     }); | 
 |     let got_textupdate_event = false; | 
 |     div.editContext.addEventListener("textupdate", e => { | 
 |       got_textupdate_event = true; | 
 |     }); | 
 |  | 
 |     div.editContext = null; | 
 |     await test_driver.send_keys(div, "a"); | 
 |  | 
 |     assert_false(got_textupdate_event, "Shouldn't have received textupdate event after editContext was detached"); | 
 |     assert_false(got_before_input_event, "Shouldn't have received beforeinput event after editContext was detached"); | 
 |  | 
 |     div.remove(); | 
 |   }, "EditContext should not receive events after being detached from element"); | 
 |  | 
 |   async function testBackspaceAndDelete(element) { | 
 |       const editContext = new EditContext(); | 
 |       let textForView = "hello there"; | 
 |       document.body.appendChild(element); | 
 |       let beforeInputType = null; | 
 |       let beforeInputTargetRanges = null; | 
 |       element.addEventListener("beforeinput", e => { | 
 |         beforeInputType = e.inputType; | 
 |         beforeInputTargetRanges = e.getTargetRanges().map( | 
 |           staticRange => [staticRange.startOffset, staticRange.endOffset]); | 
 |       }); | 
 |       let textUpdateSelection = null; | 
 |       editContext.addEventListener("textupdate", e => { | 
 |         textUpdateSelection = [e.selectionStart, e.selectionEnd]; | 
 |         textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`; | 
 |       }); | 
 |       element.editContext = editContext; | 
 |       editContext.updateText(0, 11, "hello there"); | 
 |       editContext.updateSelection(10, 10); | 
 |       const selection = window.getSelection(); | 
 |  | 
 |       await test_driver.send_keys(element, kBackspaceKey); | 
 |       assert_equals(textForView, "hello thee"); | 
 |       assert_array_equals(textUpdateSelection, [9, 9]); | 
 |       assert_equals(beforeInputType, "deleteContentBackward"); | 
 |       assert_equals(beforeInputTargetRanges.length, 0, "Backspace should not have a target range in EditContext"); | 
 |  | 
 |       await test_driver.send_keys(element, kDeleteKey); | 
 |       assert_equals(textForView, "hello the"); | 
 |       assert_array_equals(textUpdateSelection, [9, 9]); | 
 |       assert_equals(beforeInputType, "deleteContentForward"); | 
 |       assert_equals(beforeInputTargetRanges.length, 0, "Delete should not have a target range in EditContext"); | 
 |       element.remove(); | 
 |     } | 
 |  | 
 |     promise_test(testBackspaceAndDelete.bind(null, document.createElement("div")), "Backspace and delete in EditContext with div"); | 
 |     promise_test(testBackspaceAndDelete.bind(null, document.createElement("canvas")) , "Backspace and delete in EditContext with canvas"); | 
 |  | 
 |     async function testBackspaceAndDeleteWithExistingSelection(element) { | 
 |       const editContext = new EditContext(); | 
 |       let textForView = "hello there"; | 
 |       document.body.appendChild(element); | 
 |       let beforeInputType = null; | 
 |       let beforeInputTargetRanges = null; | 
 |       element.addEventListener("beforeinput", e => { | 
 |         beforeInputType = e.inputType; | 
 |         beforeInputTargetRanges = e.getTargetRanges().map( | 
 |           staticRange => [staticRange.startOffset, staticRange.endOffset]); | 
 |       }); | 
 |       let textUpdateSelection = null; | 
 |       editContext.addEventListener("textupdate", e => { | 
 |         textUpdateSelection = [e.selectionStart, e.selectionEnd]; | 
 |         textForView = `${textForView.substring(0, e.updateRangeStart)}${e.text}${textForView.substring(e.updateRangeEnd)}`; | 
 |       }); | 
 |       element.editContext = editContext; | 
 |       const initialText = "abcdefghijklmnopqrstuvwxyz"; | 
 |       editContext.updateText(0, initialText.length, initialText); | 
 |       textForView = initialText; | 
 |       element.focus(); | 
 |  | 
 |       editContext.updateSelection(3, 6); | 
 |       await test_driver.send_keys(element, kBackspaceKey); | 
 |       assert_equals(editContext.text, "abcghijklmnopqrstuvwxyz"); | 
 |       assert_equals(textForView, "abcghijklmnopqrstuvwxyz"); | 
 |       assert_array_equals(textUpdateSelection, [3, 3]); | 
 |       assert_equals(beforeInputType, "deleteContentBackward"); | 
 |       assert_equals(beforeInputTargetRanges.length, 0, "Backspace should not have a target range in EditContext"); | 
 |  | 
 |       editContext.updateSelection(3, 6); | 
 |       await test_driver.send_keys(element, kDeleteKey); | 
 |       assert_equals(editContext.text, "abcjklmnopqrstuvwxyz"); | 
 |       assert_equals(textForView, "abcjklmnopqrstuvwxyz"); | 
 |       assert_array_equals(textUpdateSelection, [3, 3]); | 
 |       assert_equals(beforeInputType, "deleteContentForward"); | 
 |       assert_equals(beforeInputTargetRanges.length, 0, "Delete should not have a target range in EditContext"); | 
 |  | 
 |       editContext.updateSelection(6, 3); | 
 |       await test_driver.send_keys(element, kBackspaceKey); | 
 |       assert_equals(editContext.text, "abcmnopqrstuvwxyz"); | 
 |       assert_equals(textForView, "abcmnopqrstuvwxyz"); | 
 |       assert_array_equals(textUpdateSelection, [3, 3]); | 
 |       assert_equals(beforeInputType, "deleteContentBackward"); | 
 |       assert_equals(beforeInputTargetRanges.length, 0, "Backspace should not have a target range in EditContext"); | 
 |  | 
 |       editContext.updateSelection(6, 3); | 
 |       await test_driver.send_keys(element, kDeleteKey); | 
 |       assert_equals(editContext.text, "abcpqrstuvwxyz"); | 
 |       assert_equals(textForView, "abcpqrstuvwxyz"); | 
 |       assert_array_equals(textUpdateSelection, [3, 3]); | 
 |       assert_equals(beforeInputType, "deleteContentForward"); | 
 |       assert_equals(beforeInputTargetRanges.length, 0, "Delete should not have a target range in EditContext"); | 
 |  | 
 |       element.remove(); | 
 |     } | 
 |  | 
 |     promise_test(testBackspaceAndDeleteWithExistingSelection.bind(null, document.createElement("div")), "Backspace and delete with existing selection with div"); | 
 |     promise_test(testBackspaceAndDeleteWithExistingSelection.bind(null, document.createElement("canvas")) , "Backspace and delete with existing selection with canvas"); | 
 |  | 
 |     promise_test(async function() { | 
 |       const iframe = document.createElement("iframe"); | 
 |       document.body.appendChild(iframe); | 
 |       const editContext = new EditContext(); | 
 |       iframe.contentDocument.body.editContext = editContext; | 
 |       iframe.contentDocument.body.focus(); | 
 |       let got_textupdate_event = false; | 
 |       editContext.addEventListener("textupdate", e => { | 
 |         got_textupdate_event = true; | 
 |       }); | 
 |       await test_driver.send_keys(iframe.contentDocument.body, "a"); | 
 |       assert_equals(iframe.contentDocument.body.innerHTML, "", "EditContext should disable DOM modification in iframe."); | 
 |       assert_true(got_textupdate_event, "Input in iframe EditContext should trigger textupdate event"); | 
 |       iframe.remove(); | 
 |     }, 'EditContext constructed outside iframe can be used in iframe'); | 
 |  | 
 |     promise_test(async function () { | 
 |       const div = document.createElement("div"); | 
 |       document.body.appendChild(div); | 
 |       const editContext = new EditContext(); | 
 |       div.editContext = editContext; | 
 |       let textupdateEventCount = 0; | 
 |       editContext.addEventListener("textupdate", e => { | 
 |         textupdateEventCount++; | 
 |       }); | 
 |  | 
 |       div.focus(); | 
 |       await test_driver.send_keys(div, 'a'); | 
 |       assert_equals(textupdateEventCount, 1); | 
 |       assert_equals(div.innerHTML, ''); | 
 |  | 
 |       const iframe = document.createElement('iframe'); | 
 |       document.body.appendChild(iframe); | 
 |       iframe.contentDocument.body.appendChild(div); | 
 |  | 
 |       div.focus(); | 
 |       await test_driver.send_keys(div, 'b'); | 
 |       assert_equals(textupdateEventCount, 2); | 
 |       assert_equals(div.innerHTML, ''); | 
 |  | 
 |       iframe.remove(); | 
 |     }, 'Textupdate event should be fired on edit context when the editor element is moved to an iframe'); | 
 |  | 
 |     promise_test(async function() { | 
 |       const div = document.createElement("div"); | 
 |       const input = document.createElement("input"); | 
 |       document.body.appendChild(div); | 
 |       document.body.appendChild(input); | 
 |       const editContext = new EditContext(); | 
 |       div.editContext = editContext; | 
 |       div.focus(); | 
 |       div.remove(); | 
 |       input.focus(); | 
 |       await test_driver.send_keys(input, "a"); | 
 |       assert_equals(input.value, "a", "input should have received text input"); | 
 |  | 
 |       input.remove(); | 
 |     }, 'Removing EditContext-associated element with focus doesn\'t prevent further text input on the page'); | 
 |   </script> | 
 | </body> | 
 | </html> |