copypaste.js (18372B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 function modifySelection(s) { 5 var g = window.getSelection(); 6 var l = g.getRangeAt(0); 7 var d = document.createElement("p"); 8 d.innerHTML = s; 9 d.appendChild(l.cloneContents()); 10 11 var e = document.createElement("div"); 12 document.body.appendChild(e); 13 e.appendChild(d); 14 var a = document.createRange(); 15 a.selectNode(d); 16 g.removeAllRanges(); 17 g.addRange(a); 18 window.setTimeout(function () { 19 e.remove(); 20 g.removeAllRanges(); 21 g.addRange(l); 22 }, 0); 23 } 24 25 function getLoadContext() { 26 var Ci = SpecialPowers.Ci; 27 return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext); 28 } 29 30 async function testCopyPaste(isXHTML) { 31 var suppressUnicodeCheckIfHidden = !!isXHTML; 32 var suppressHTMLCheck = !!isXHTML; 33 34 var docShell = SpecialPowers.wrap(window).docShell; 35 36 var documentViewer = docShell.docViewer.QueryInterface( 37 SpecialPowers.Ci.nsIDocumentViewerEdit 38 ); 39 40 var clipboard = SpecialPowers.Services.clipboard; 41 42 var textarea = SpecialPowers.wrap(document.getElementById("input")); 43 44 async function copySelectionToClipboard(suppressUnicodeCheck) { 45 await SimpleTest.promiseClipboardChange( 46 () => true, 47 () => { 48 documentViewer.copySelection(); 49 } 50 ); 51 if (!suppressUnicodeCheck) { 52 ok( 53 clipboard.hasDataMatchingFlavors(["text/plain"], 1), 54 "check text/plain" 55 ); 56 } 57 if (!suppressHTMLCheck) { 58 ok(clipboard.hasDataMatchingFlavors(["text/html"], 1), "check text/html"); 59 } 60 } 61 function clear() { 62 textarea.blur(); 63 var sel = window.getSelection(); 64 sel.removeAllRanges(); 65 } 66 async function copyToClipboard(node, suppressUnicodeCheck) { 67 clear(); 68 var r = document.createRange(); 69 r.selectNode(node); 70 window.getSelection().addRange(r); 71 await copySelectionToClipboard(suppressUnicodeCheck); 72 } 73 function addRange(startNode, startIndex, endNode, endIndex) { 74 var sel = window.getSelection(); 75 var r = document.createRange(); 76 r.setStart(startNode, startIndex); 77 r.setEnd(endNode, endIndex); 78 sel.addRange(r); 79 } 80 async function copyRangeToClipboard( 81 startNode, 82 startIndex, 83 endNode, 84 endIndex, 85 suppressUnicodeCheck 86 ) { 87 clear(); 88 addRange(startNode, startIndex, endNode, endIndex); 89 await copySelectionToClipboard(suppressUnicodeCheck); 90 } 91 async function copyChildrenToClipboard(id) { 92 clear(); 93 window.getSelection().selectAllChildren(document.getElementById(id)); 94 await copySelectionToClipboard(); 95 } 96 function getClipboardData(mime) { 97 var transferable = SpecialPowers.Cc[ 98 "@mozilla.org/widget/transferable;1" 99 ].createInstance(SpecialPowers.Ci.nsITransferable); 100 transferable.init(getLoadContext()); 101 transferable.addDataFlavor(mime); 102 clipboard.getData( 103 transferable, 104 1, 105 SpecialPowers.wrap(window).browsingContext.currentWindowContext 106 ); 107 var data = SpecialPowers.createBlankObject(); 108 transferable.getTransferData(mime, data); 109 return data; 110 } 111 function testHtmlClipboardValue(mime, expected) { 112 // For Windows, navigator.platform returns "Win32". 113 var expectedValue = expected; 114 if (navigator.platform.includes("Win")) { 115 // Windows has extra content. 116 expectedValue = 117 kTextHtmlPrefixClipboardDataWindows + 118 expected.replace(/\n/g, "\n") + 119 kTextHtmlSuffixClipboardDataWindows; 120 } 121 testClipboardValue(mime, expectedValue); 122 } 123 function testClipboardValue(mime, expected) { 124 if (suppressHTMLCheck && mime == "text/html") { 125 return null; 126 } 127 var data = SpecialPowers.wrap(getClipboardData(mime)); 128 is( 129 data.value == null 130 ? data.value 131 : data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data, 132 expected, 133 mime + " value in the clipboard" 134 ); 135 return data.value; 136 } 137 function testPasteText(expected) { 138 textarea.value = ""; 139 textarea.focus(); 140 textarea.editor.paste(1); 141 is(textarea.value, expected, "value of the textarea after the paste"); 142 } 143 function testPasteHTML(id, expected) { 144 var contentEditable = $(id); 145 contentEditable.focus(); 146 synthesizeKey("v", { accelKey: true }); 147 is(contentEditable.innerHTML, expected, id + ".innerHtml after the paste"); 148 } 149 function testSelectionToString(expected) { 150 is( 151 window.getSelection().toString().replace(/\r\n/g, "\n"), 152 expected, 153 "Selection.toString" 154 ); 155 } 156 function testInnerHTML(id, expected) { 157 var value = document.getElementById(id).innerHTML; 158 is(value, expected, id + ".innerHTML"); 159 } 160 161 const includeCommonAncestor = SpecialPowers.getBoolPref( 162 "dom.serializer.includeCommonAncestor.enabled" 163 ); 164 165 await copyChildrenToClipboard("draggable"); 166 testSelectionToString("This is a draggable bit of text."); 167 testClipboardValue("text/plain", "This is a draggable bit of text."); 168 testHtmlClipboardValue( 169 "text/html", 170 `${includeCommonAncestor ? '<div id="draggable" title="title to have a long HTML line">' : ""}` + 171 `This is a <em>draggable</em> bit of text.` + 172 `${includeCommonAncestor ? "</div>" : ""}` 173 ); 174 testPasteText("This is a draggable bit of text."); 175 176 await copyChildrenToClipboard("alist"); 177 testSelectionToString(" bla\n\n foo\n bar\n\n"); 178 testClipboardValue("text/plain", " bla\n\n foo\n bar\n\n"); 179 testHtmlClipboardValue( 180 "text/html", 181 `${includeCommonAncestor ? '<div id="alist">' : ""}` + 182 `\n bla\n <ul>\n <li>foo</li>\n \n <li>bar</li>\n </ul>\n ` + 183 `${includeCommonAncestor ? "</div>" : ""}` 184 ); 185 testPasteText(" bla\n\n foo\n bar\n\n"); 186 187 await copyChildrenToClipboard("blist"); 188 testSelectionToString(" mozilla\n\n foo\n bar\n\n"); 189 testClipboardValue("text/plain", " mozilla\n\n foo\n bar\n\n"); 190 testHtmlClipboardValue( 191 "text/html", 192 `${includeCommonAncestor ? '<div id="blist">' : ""}` + 193 `\n mozilla\n <ol>\n <li>foo</li>\n \n <li>bar</li>\n </ol>\n ` + 194 `${includeCommonAncestor ? "</div>" : ""}` 195 ); 196 testPasteText(" mozilla\n\n foo\n bar\n\n"); 197 198 await copyChildrenToClipboard("clist"); 199 testSelectionToString(" mzla\n\n foo\n bazzinga!\n bar\n\n"); 200 testClipboardValue( 201 "text/plain", 202 " mzla\n\n foo\n bazzinga!\n bar\n\n" 203 ); 204 testHtmlClipboardValue( 205 "text/html", 206 `${includeCommonAncestor ? '<div id="clist">' : ""}` + 207 `\n mzla\n <ul>\n <li>foo<ul>\n <li>bazzinga!</li>\n </ul></li>\n \n <li>bar</li>\n </ul>\n ` + 208 `${includeCommonAncestor ? "</div>" : ""}` 209 ); 210 testPasteText(" mzla\n\n foo\n bazzinga!\n bar\n\n"); 211 212 await copyChildrenToClipboard("div4"); 213 testSelectionToString(" Tt t t "); 214 testClipboardValue("text/plain", " Tt t t "); 215 if (isXHTML) { 216 testHtmlClipboardValue( 217 "text/html", 218 '<div id="div4">\n T<textarea xmlns="http://www.w3.org/1999/xhtml">t t t</textarea>\n</div>' 219 ); 220 testInnerHTML( 221 "div4", 222 '\n T<textarea xmlns="http://www.w3.org/1999/xhtml">t t t</textarea>\n' 223 ); 224 } else { 225 testHtmlClipboardValue( 226 "text/html", 227 `${includeCommonAncestor ? '<div id="div4">' : ""}` + 228 `\n T<textarea>t t t</textarea>\n` + 229 `${includeCommonAncestor ? "</div>" : ""}` 230 ); 231 testInnerHTML("div4", "\n T<textarea>t t t</textarea>\n"); 232 } 233 testPasteText(" Tt t t "); 234 235 await copyChildrenToClipboard("div5"); 236 testSelectionToString(" T "); 237 testClipboardValue("text/plain", " T "); 238 if (isXHTML) { 239 testHtmlClipboardValue( 240 "text/html", 241 '<div id="div5">\n T<textarea xmlns="http://www.w3.org/1999/xhtml"> </textarea>\n</div>' 242 ); 243 testInnerHTML( 244 "div5", 245 '\n T<textarea xmlns="http://www.w3.org/1999/xhtml"> </textarea>\n' 246 ); 247 } else { 248 testHtmlClipboardValue( 249 "text/html", 250 `${includeCommonAncestor ? '<div id="div5">' : ""}` + 251 `\n T<textarea> </textarea>\n` + 252 `${includeCommonAncestor ? "</div>" : ""}` 253 ); 254 testInnerHTML("div5", "\n T<textarea> </textarea>\n"); 255 } 256 testPasteText(" T "); 257 258 await copyRangeToClipboard( 259 $("div6").childNodes[0], 260 0, 261 $("div6").childNodes[1], 262 1, 263 suppressUnicodeCheckIfHidden 264 ); 265 testSelectionToString(""); 266 // XXX: disabled due to bug 564688 267 // testClipboardValue("text/plain", ""); 268 // testClipboardValue("text/html", ""); 269 testInnerHTML("div6", "div6"); 270 271 await copyRangeToClipboard( 272 $("div7").childNodes[0], 273 0, 274 $("div7").childNodes[0], 275 4, 276 suppressUnicodeCheckIfHidden 277 ); 278 testSelectionToString(""); 279 // XXX: disabled due to bug 564688 280 // testClipboardValue("text/plain", ""); 281 // testClipboardValue("text/html", ""); 282 testInnerHTML("div7", "div7"); 283 284 await copyRangeToClipboard( 285 $("div8").childNodes[0], 286 0, 287 $("div8").childNodes[0], 288 4, 289 suppressUnicodeCheckIfHidden 290 ); 291 testSelectionToString(""); 292 // XXX: disabled due to bug 564688 293 // testClipboardValue("text/plain", ""); 294 // testClipboardValue("text/html", ""); 295 testInnerHTML("div8", "div8"); 296 297 await copyRangeToClipboard( 298 $("div9").childNodes[0], 299 0, 300 $("div9").childNodes[0], 301 4, 302 suppressUnicodeCheckIfHidden 303 ); 304 testSelectionToString("div9"); 305 testClipboardValue("text/plain", "div9"); 306 testHtmlClipboardValue("text/html", "div9"); 307 testInnerHTML("div9", "div9"); 308 309 await copyToClipboard($("div10"), suppressUnicodeCheckIfHidden); 310 testSelectionToString(""); 311 testInnerHTML("div10", "div10"); 312 313 await copyToClipboard($("div10").firstChild, suppressUnicodeCheckIfHidden); 314 testSelectionToString(""); 315 316 await copyRangeToClipboard( 317 $("div10").childNodes[0], 318 0, 319 $("div10").childNodes[0], 320 1, 321 suppressUnicodeCheckIfHidden 322 ); 323 testSelectionToString(""); 324 325 await copyRangeToClipboard( 326 $("div10").childNodes[1], 327 0, 328 $("div10").childNodes[1], 329 1, 330 suppressUnicodeCheckIfHidden 331 ); 332 testSelectionToString(""); 333 334 if (!isXHTML) { 335 // ============ copy/paste multi-range selection (bug 1123505) 336 // with text start node 337 var sel = window.getSelection(); 338 sel.removeAllRanges(); 339 var r = document.createRange(); 340 var ul = $("ul1"); 341 var parent = ul.parentNode; 342 r.setStart(parent, 0); 343 r.setEnd(parent.firstChild, 15); 344 sel.addRange(r); // <div>{Copy1then Paste]<ul id="ul1"><li>LI</li>\n</ul></div> 345 346 r = document.createRange(); 347 r.setStart(ul, 1); 348 r.setEnd(parent, 2); 349 sel.addRange(r); // <div>Copy1then Paste<ul id="ul1"><li>LI{</li>\n</ul>}</div> 350 await copySelectionToClipboard(true); 351 testPasteHTML("contentEditable1", "Copy1then Paste"); // The <ul> should not appear because it has no <li>s 352 353 // with text end node 354 sel = window.getSelection(); 355 sel.removeAllRanges(); 356 r = document.createRange(); 357 ul = $("ul2"); 358 parent = ul.parentNode; 359 r.setStart(parent, 0); 360 r.setEnd(ul, 1); 361 sel.addRange(r); // <div>{<ul id="ul2">\n}<li>LI</li></ul>Copy2then Paste</div> 362 363 r = document.createRange(); 364 r.setStart(parent.childNodes[1], 0); 365 r.setEnd(parent, 2); 366 sel.addRange(r); // <div><ul id="ul2">\n<li>LI</li></ul>[Copy2then Paste}</div> 367 await copySelectionToClipboard(true); 368 testPasteHTML("contentEditable2", "Copy2then Paste"); // The <ul> should not appear because it has no <li>s 369 370 // with text end node and non-empty start 371 sel = window.getSelection(); 372 sel.removeAllRanges(); 373 r = document.createRange(); 374 ul = $("ul3"); 375 parent = ul.parentNode; 376 r.setStart(parent, 0); 377 r.setEnd(ul, 1); 378 sel.addRange(r); // <div>{<ul id="ul3"><li>\n</li>}<li>LI</li></ul>Copy3then Paste</div> 379 380 r = document.createRange(); 381 r.setStart(parent.childNodes[1], 0); 382 r.setEnd(parent, 2); 383 sel.addRange(r); // <div><ul id="ul3"><li>\n</li><li>LI</li></ul>[Copy3then Paste}</div> 384 await copySelectionToClipboard(true); 385 testPasteHTML( 386 "contentEditable3", 387 // The <ul> should appear because it has a <li> 388 // The preceding linefeed of the <br> in the empty <li> is an invisible 389 // white-space. Thus, it's not important whether it appears or not in the result. 390 '<ul id="ul3"><li><br></li></ul>Copy3then Paste' 391 ); 392 393 // with elements of different depth 394 sel = window.getSelection(); 395 sel.removeAllRanges(); 396 r = document.createRange(); 397 var div1 = $("div1s"); 398 parent = div1.parentNode; 399 r.setStart(parent, 0); 400 r.setEnd(document.getElementById("div1se1"), 1); // after the "inner" DIV 401 sel.addRange(r); 402 403 r = document.createRange(); 404 r.setStart(div1.childNodes[1], 0); // the start of "after" 405 r.setEnd(parent, 1); 406 sel.addRange(r); 407 await copySelectionToClipboard(true); 408 testPasteHTML( 409 "contentEditable4", 410 '<div id="div1s"><div id="div1se1">before</div></div><div id="div1s">after</div>' 411 ); 412 413 // with elements of different depth, and a text node at the end 414 sel = window.getSelection(); 415 sel.removeAllRanges(); 416 r = document.createRange(); 417 div1 = $("div2s"); 418 parent = div1.parentNode; 419 r.setStart(parent, 0); 420 r.setEnd(document.getElementById("div2se1"), 1); // after the "inner" DIV 421 sel.addRange(r); 422 423 r = document.createRange(); 424 r.setStart(div1.childNodes[1], 0); // the start of "after" 425 r.setEnd(parent, 1); 426 sel.addRange(r); 427 await copySelectionToClipboard(true); 428 testPasteHTML( 429 "contentEditable5", 430 '<div id="div2s"><div id="div2se1">before</div></div><div id="div2s">after</div>' 431 ); 432 433 // crash test for bug 1127835 434 var e1 = document.getElementById("1127835crash1"); 435 var e2 = document.getElementById("1127835crash2"); 436 var e3 = document.getElementById("1127835crash3"); 437 var t1 = e1.childNodes[0]; 438 var t3 = e3.childNodes[0]; 439 440 sel = window.getSelection(); 441 sel.removeAllRanges(); 442 443 r = document.createRange(); 444 r.setStart(t1, 1); 445 r.setEnd(e2, 0); 446 sel.addRange(r); // <div>\n<span id="1127835crash1">1[</span><div id="1127835crash2">}<div>\n</div></div><a href="..." id="1127835crash3">3</a>\n</div> 447 448 r = document.createRange(); 449 r.setStart(e2, 1); 450 r.setEnd(t3, 0); 451 sel.addRange(r); // <div>\n<span id="1127835crash1">1</span><div id="1127835crash2"><div>\n</div>{</div><a href="..." id="1127835crash3">]3</a>\n</div> 452 await copySelectionToClipboard(true); 453 testPasteHTML( 454 "contentEditable6", 455 '<span id="1127835crash1"></span><div id="1127835crash2"><div>\n</div></div><a href="http://www.mozilla.org/" id="1127835crash3"><br></a>' 456 ); // Don't strip the empty `<a href="...">` element because of avoiding any dataloss provided by the element 457 } 458 459 // ============ copy/paste test from/to a textarea 460 461 var val = "1\n 2\n 3"; 462 textarea.value = val; 463 textarea.select(); 464 await SimpleTest.promiseClipboardChange(textarea.value, () => { 465 textarea.editor.copy(); 466 }); 467 textarea.value = ""; 468 textarea.editor.paste(1); 469 is(textarea.value, val); 470 textarea.value = ""; 471 472 // ============ NOSCRIPT should not be copied 473 474 await copyChildrenToClipboard("div13"); 475 testSelectionToString("__"); 476 testClipboardValue("text/plain", "__"); 477 testHtmlClipboardValue( 478 "text/html", 479 `${includeCommonAncestor ? '<div id="div13">' : ""}` + 480 `__` + 481 `${includeCommonAncestor ? "</div>" : ""}` 482 ); 483 testPasteText("__"); 484 485 // ============ converting cell boundaries to tabs in tables 486 487 await copyToClipboard($("tr1")); 488 testClipboardValue("text/plain", "foo\tbar"); 489 490 if (!isXHTML) { 491 // ============ spanning multiple rows 492 493 await copyRangeToClipboard($("tr2"), 0, $("tr3"), 0); 494 testClipboardValue("text/plain", "1\t2\n3\t4\n"); 495 testHtmlClipboardValue( 496 "text/html", 497 '<table><tbody><tr id="tr2"><tr id="tr2"><td>1</td><td>2</td></tr><tr><td>3</td><td>4</td></tr><tr id="tr3"></tr></tr></tbody></table>' 498 ); 499 500 // ============ spanning multiple rows in multi-range selection 501 502 clear(); 503 addRange($("tr2"), 0, $("tr2"), 2); 504 addRange($("tr3"), 0, $("tr3"), 2); 505 await copySelectionToClipboard(); 506 testClipboardValue("text/plain", "1\t2\n5\t6"); 507 testHtmlClipboardValue( 508 "text/html", 509 '<table><tbody><tr id="tr2"><td>1</td><td>2</td></tr><tr id="tr3"><td>5</td><td>6</td></tr></tbody></table>' 510 ); 511 } 512 513 // ============ manipulating Selection in oncopy 514 515 await copyRangeToClipboard( 516 $("div11").childNodes[0], 517 0, 518 $("div11").childNodes[1], 519 2 520 ); 521 testClipboardValue("text/plain", "Xdiv11"); 522 testHtmlClipboardValue( 523 "text/html", 524 `${includeCommonAncestor ? "<div>" : ""}` + 525 `<p>X<span>div</span>11</p>` + 526 `${includeCommonAncestor ? "</div>" : ""}` 527 ); 528 529 await new Promise(resolve => { 530 setTimeout(resolve, 0); 531 }); 532 testSelectionToString("div11"); 533 534 await new Promise(resolve => { 535 setTimeout(resolve, 0); 536 }); 537 await copyRangeToClipboard( 538 $("div12").childNodes[0], 539 0, 540 $("div12").childNodes[1], 541 2 542 ); 543 544 testClipboardValue("text/plain", "Xdiv12"); 545 testHtmlClipboardValue( 546 "text/html", 547 `${includeCommonAncestor ? "<div>" : ""}` + 548 `<p>X<span>div</span>12</p>` + 549 `${includeCommonAncestor ? "</div>" : ""}` 550 ); 551 await new Promise(resolve => { 552 setTimeout(resolve, 0); 553 }); 554 testSelectionToString("div12"); 555 556 await new Promise(resolve => { 557 setTimeout(resolve, 0); 558 }); 559 560 if (!isXHTML) { 561 // ============ copy from ruby 562 563 const ruby1 = $("ruby1"); 564 const ruby1Container = ruby1.parentNode; 565 566 // Ruby annotation is included when selecting inside ruby. 567 await copyRangeToClipboard(ruby1, 0, ruby1, 6); 568 testClipboardValue("text/plain", "aabb(AABB)"); 569 570 // Ruby annotation is ignored when selecting across ruby. 571 await copyRangeToClipboard(ruby1Container, 0, ruby1Container, 3); 572 testClipboardValue("text/plain", "XaabbY"); 573 574 // ... unless converter.html2txt.always_include_ruby is set 575 await SpecialPowers.pushPrefEnv({ 576 set: [["converter.html2txt.always_include_ruby", true]], 577 }); 578 await copyRangeToClipboard(ruby1Container, 0, ruby1Container, 3); 579 testClipboardValue("text/plain", "Xaabb(AABB)Y"); 580 await SpecialPowers.popPrefEnv(); 581 } 582 }